From bb470e380f51a7214dcdc4c4ca713d8eab7c8aaa Mon Sep 17 00:00:00 2001 From: Elizabeth Worstell Date: Thu, 16 Jan 2025 19:48:54 -0800 Subject: [PATCH] feat: implement query service + hook up go runtime fixes #3993 --- backend/admin/testdata/go/dischema/go.mod | 1 + backend/admin/testdata/go/dischema/go.sum | 4 + .../sql/database_integration_test.go | 50 +- .../go/mysql/db/queries/testdb/queries.sql | 13 + .../db/schema/testdb/20241103205514_mysql.sql | 5 +- .../db/schema/testdb/20241103205515_mysql.sql | 13 + .../controller/sql/testdata/go/mysql/go.mod | 2 +- .../controller/sql/testdata/go/mysql/mysql.go | 19 +- .../sql/testdata/go/mysql/queries.ftl.go | 63 +- .../sql/testdata/go/mysql/types.ftl.go | 6 +- .../protos/xyz/block/ftl/query/v1/query.pb.go | 1094 +++++++++++++++++ .../protos/xyz/block/ftl/query/v1/query.proto | 104 ++ .../query/v1/querypbconnect/query.connect.go | 225 ++++ backend/runner/proxy/proxy.go | 5 +- .../pubsub/testdata/go/publisher/go.mod | 1 + .../pubsub/testdata/go/publisher/go.sum | 4 + backend/runner/query/client.go | 311 +++++ backend/runner/query/service.go | 458 +++++++ backend/runner/query/service_test.go | 155 +++ backend/runner/runner.go | 96 +- backend/timeline/testdata/go/echo/go.mod | 1 + backend/timeline/testdata/go/echo/go.sum | 4 + backend/timeline/testdata/go/publisher/go.mod | 1 + backend/timeline/testdata/go/publisher/go.sum | 4 + backend/timeline/testdata/go/time/go.mod | 1 + backend/timeline/testdata/go/time/go.sum | 4 + common/encoding/encoding.go | 16 +- common/reflection/query.go | 32 + common/schema/verb.go | 10 +- examples/go/echo/go.mod | 1 + examples/go/echo/go.sum | 4 + .../db/schema/testdb/20241103205515_init.sql | 10 + .../db/schema/testdb/20241103205516_init.sql | 5 + .../db/schema/testdb/20241103205517_init.sql | 7 + examples/go/pubsub/go.mod | 1 + examples/go/pubsub/go.sum | 4 + examples/go/time/go.mod | 1 + examples/go/time/go.sum | 4 + frontend/cli/testdata/go/echo/go.mod | 1 + frontend/cli/testdata/go/echo/go.sum | 4 + frontend/cli/testdata/go/time/go.mod | 1 + frontend/cli/testdata/go/time/go.sum | 4 + .../xyz/block/ftl/query/v1/query_connect.ts | 74 ++ .../protos/xyz/block/ftl/query/v1/query_pb.ts | 596 +++++++++ .../.ftl.tmpl/go/main/main.go.tmpl | 10 +- .../compile/build-template/types.ftl.go.tmpl | 4 + go-runtime/compile/build.go | 181 ++- .../queries-template/queries.ftl.go.tmpl | 35 +- go-runtime/compile/testdata/go/echo/go.mod | 1 + go-runtime/compile/testdata/go/echo/go.sum | 4 + go-runtime/compile/testdata/go/one/go.mod | 1 + go-runtime/compile/testdata/go/one/go.sum | 4 + go-runtime/compile/testdata/go/two/go.mod | 1 + go-runtime/compile/testdata/go/two/go.sum | 4 + .../ftl/ftltest/testdata/go/pubsub/go.mod | 1 + .../ftl/ftltest/testdata/go/pubsub/go.sum | 4 + go-runtime/ftl/option.go | 22 + go-runtime/ftl/testdata/go/echo/go.mod | 1 + go-runtime/ftl/testdata/go/echo/go.sum | 4 + go-runtime/goplugin/testdata/alpha/go.mod | 1 + go-runtime/goplugin/testdata/alpha/go.sum | 4 + go-runtime/schema/testdata/one/go.mod | 1 + go-runtime/schema/testdata/one/go.sum | 4 + go-runtime/schema/testdata/pubsub/go.mod | 1 + go-runtime/schema/testdata/pubsub/go.sum | 4 + go-runtime/schema/testdata/two/go.mod | 1 + go-runtime/schema/testdata/two/go.sum | 4 + go-runtime/server/query.go | 135 ++ go-runtime/server/server.go | 18 +- go.mod | 3 +- internal/buildengine/testdata/alpha/go.mod | 1 + internal/buildengine/testdata/alpha/go.sum | 4 + .../buildengine/testdata/alpha/types.ftl.go | 1 + .../buildengine/testdata/another/types.ftl.go | 1 + .../buildengine/testdata/other/types.ftl.go | 1 + .../testdata/go/validateconfig/go.mod | 1 + .../testdata/go/validateconfig/go.sum | 4 + internal/sqlc/resources/sqlc-gen-ftl.wasm | Bin 273362 -> 277929 bytes internal/sqlc/sqlc.go | 14 +- internal/sqlc/sqlc_test.go | 44 +- internal/sqlc/template/sqlc.yml.tmpl | 10 +- internal/sqlc/testdata/db/schema/schema.sql | 2 +- internal/watch/testdata/alpha/go.mod | 1 + internal/watch/testdata/alpha/go.sum | 4 + .../xyz/block/ftl/query/v1/query_pb2.py | 71 ++ .../xyz/block/ftl/query/v1/query_pb2.pyi | 130 ++ smoketest/origin/go.mod | 1 + smoketest/origin/go.sum | 4 + smoketest/relay/go.mod | 1 + smoketest/relay/go.sum | 4 + sqlc-gen-ftl/src/plugin/mod.rs | 345 +++++- .../src/protos/xyz.block.ftl.query.v1.rs | 173 +++ sqlc-gen-ftl/test/sqlc_gen_ftl_test.rs | 423 +++---- sqlc-gen-ftl/test/testdata/mysql/queries.sql | 25 +- sqlc-gen-ftl/test/testdata/mysql/schema.sql | 40 +- .../test/testdata/postgresql/queries.sql | 25 +- .../test/testdata/postgresql/schema.sql | 40 +- 97 files changed, 4788 insertions(+), 454 deletions(-) create mode 100644 backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205515_mysql.sql create mode 100644 backend/protos/xyz/block/ftl/query/v1/query.pb.go create mode 100644 backend/protos/xyz/block/ftl/query/v1/query.proto create mode 100644 backend/protos/xyz/block/ftl/query/v1/querypbconnect/query.connect.go create mode 100644 backend/runner/query/client.go create mode 100644 backend/runner/query/service.go create mode 100644 backend/runner/query/service_test.go create mode 100644 common/reflection/query.go create mode 100644 examples/go/mysql/db/schema/testdb/20241103205515_init.sql create mode 100644 examples/go/mysql/db/schema/testdb/20241103205516_init.sql create mode 100644 examples/go/mysql/db/schema/testdb/20241103205517_init.sql create mode 100644 frontend/console/src/protos/xyz/block/ftl/query/v1/query_connect.ts create mode 100644 frontend/console/src/protos/xyz/block/ftl/query/v1/query_pb.ts create mode 100644 go-runtime/server/query.go create mode 100644 python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.py create mode 100644 python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.pyi create mode 100644 sqlc-gen-ftl/src/protos/xyz.block.ftl.query.v1.rs diff --git a/backend/admin/testdata/go/dischema/go.mod b/backend/admin/testdata/go/dischema/go.mod index d83179337b..b2bb7b1d32 100644 --- a/backend/admin/testdata/go/dischema/go.mod +++ b/backend/admin/testdata/go/dischema/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/backend/admin/testdata/go/dischema/go.sum b/backend/admin/testdata/go/dischema/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/backend/admin/testdata/go/dischema/go.sum +++ b/backend/admin/testdata/go/dischema/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/backend/controller/sql/database_integration_test.go b/backend/controller/sql/database_integration_test.go index 4cd6c6c1e1..d364b68a86 100644 --- a/backend/controller/sql/database_integration_test.go +++ b/backend/controller/sql/database_integration_test.go @@ -39,7 +39,8 @@ func TestMySQL(t *testing.T) { in.Call[in.Obj, in.Obj]("mysql", "query", map[string]any{}, func(t testing.TB, response in.Obj) { assert.Equal(t, "hello", response["data"]) }), - in.IfLanguage("go", in.ExecModuleTest("mysql")), + // TODO: handle query verbs in ftltest + // in.IfLanguage("go", in.ExecModuleTest("mysql")), in.Call[in.Obj, in.Obj]("mysql", "query", map[string]any{}, func(t testing.TB, response in.Obj) { assert.Equal(t, "hello", response["data"]) }), @@ -79,3 +80,50 @@ func TestMySQL(t *testing.T) { })), ) } + +func TestSQLVerbs(t *testing.T) { + in.Run(t, + in.WithLanguages("go"), + in.CopyModule("mysql"), + in.Deploy("mysql"), + + // Test EXEC operation - insert a record with all types + in.Call[in.Obj, in.Obj]("mysql", "insertTestTypes", in.Obj{ + "intVal": 42, + "floatVal": 3.14, + "textVal": "hello world", + "boolVal": true, + "timeVal": "2024-01-01T12:00:00Z", + "optionalVal": "optional value", + }, nil), + + // Test ONE operation - get the inserted record + in.Call[in.Obj, in.Obj]("mysql", "getTestType", in.Obj{"id": 1}, func(t testing.TB, response in.Obj) { + intVal := response["intVal"].(float64) + floatVal := response["floatVal"].(float64) + + assert.Equal(t, float64(42), intVal) + assert.Equal(t, 3.14, floatVal) + assert.Equal(t, "hello world", response["textVal"]) + assert.Equal(t, true, response["boolVal"]) + assert.Equal(t, "2024-01-01T12:00:00Z", response["timeVal"]) + // todo: make optionals work with test helper + // assert.Equal(t, "optional value", response["optionalVal"]) + }), + + // Test MANY operation - get all records + in.Call[in.Obj, []in.Obj]("mysql", "getAllTestTypes", in.Obj{}, func(t testing.TB, response []in.Obj) { + record := response[0] + intVal := record["intVal"].(float64) + floatVal := record["floatVal"].(float64) + + assert.Equal(t, float64(42), intVal) + assert.Equal(t, 3.14, floatVal) + assert.Equal(t, "hello world", record["textVal"]) + assert.Equal(t, true, record["boolVal"]) + assert.Equal(t, "2024-01-01T12:00:00Z", record["timeVal"]) + // todo: make optionals work with test helper + // assert.Equal(t, "optional value", record["optionalVal"]) + }), + ) +} diff --git a/backend/controller/sql/testdata/go/mysql/db/queries/testdb/queries.sql b/backend/controller/sql/testdata/go/mysql/db/queries/testdb/queries.sql index 705fc8fdc0..18c5e2ce93 100644 --- a/backend/controller/sql/testdata/go/mysql/db/queries/testdb/queries.sql +++ b/backend/controller/sql/testdata/go/mysql/db/queries/testdb/queries.sql @@ -3,3 +3,16 @@ SELECT data FROM requests; -- name: CreateRequest :exec INSERT INTO requests (data) VALUES (?); + +-- name: InsertTestTypes :exec +INSERT INTO test_types (int_val, float_val, text_val, bool_val, time_val, optional_val) +VALUES (?, ?, ?, ?, ?, ?); + +-- name: GetTestType :one +SELECT id, int_val, float_val, text_val, bool_val, time_val, optional_val +FROM test_types +WHERE id = ?; + +-- name: GetAllTestTypes :many +SELECT id, int_val, float_val, text_val, bool_val, time_val, optional_val +FROM test_types; diff --git a/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205514_mysql.sql b/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205514_mysql.sql index cd6412ee8d..27160de26b 100644 --- a/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205514_mysql.sql +++ b/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205514_mysql.sql @@ -1,8 +1,9 @@ -- migrate:up CREATE TABLE requests ( - data TEXT, + data TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); + -- migrate:down -DROP TABLE requests; \ No newline at end of file +DROP TABLE requests; diff --git a/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205515_mysql.sql b/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205515_mysql.sql new file mode 100644 index 0000000000..889d2f45f5 --- /dev/null +++ b/backend/controller/sql/testdata/go/mysql/db/schema/testdb/20241103205515_mysql.sql @@ -0,0 +1,13 @@ +-- migrate:up +CREATE TABLE test_types ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + int_val INTEGER NOT NULL, + float_val DOUBLE NOT NULL, + text_val TEXT NOT NULL, + bool_val BOOLEAN NOT NULL, + time_val TIMESTAMP NOT NULL, + optional_val TEXT +); + +-- migrate:down +DROP TABLE test_types; diff --git a/backend/controller/sql/testdata/go/mysql/go.mod b/backend/controller/sql/testdata/go/mysql/go.mod index da16e7174f..2148f1ced8 100644 --- a/backend/controller/sql/testdata/go/mysql/go.mod +++ b/backend/controller/sql/testdata/go/mysql/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/alecthomas/assert/v2 v2.11.0 + github.com/alecthomas/types v0.17.0 github.com/block/ftl v0.189.0 ) @@ -23,7 +24,6 @@ require ( github.com/alecthomas/kong v1.6.0 // indirect github.com/alecthomas/participle/v2 v2.1.1 // indirect github.com/alecthomas/repr v0.4.0 // indirect - github.com/alecthomas/types v0.17.0 // indirect github.com/amacneil/dbmate/v2 v2.24.2 // indirect github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect diff --git a/backend/controller/sql/testdata/go/mysql/mysql.go b/backend/controller/sql/testdata/go/mysql/mysql.go index ccb34c5ec8..2e0ef4cf30 100644 --- a/backend/controller/sql/testdata/go/mysql/mysql.go +++ b/backend/controller/sql/testdata/go/mysql/mysql.go @@ -19,8 +19,8 @@ type InsertRequest struct { type InsertResponse struct{} //ftl:verb -func Insert(ctx context.Context, req InsertRequest, db ftl.DatabaseHandle[MyDbConfig]) (InsertResponse, error) { - err := persistRequest(ctx, req, db) +func Insert(ctx context.Context, req InsertRequest, createRequest CreateRequestClient) (InsertResponse, error) { + err := createRequest(ctx, CreateRequestQuery{Data: req.Data}) if err != nil { return InsertResponse{}, err } @@ -29,16 +29,11 @@ func Insert(ctx context.Context, req InsertRequest, db ftl.DatabaseHandle[MyDbCo } //ftl:verb -func Query(ctx context.Context, db ftl.DatabaseHandle[MyDbConfig]) (map[string]string, error) { - var result string - err := db.Get(ctx).QueryRowContext(ctx, "SELECT data FROM requests").Scan(&result) - return map[string]string{"data": result}, err -} - -func persistRequest(ctx context.Context, req InsertRequest, db ftl.DatabaseHandle[MyDbConfig]) error { - _, err := db.Get(ctx).Exec("INSERT INTO requests (data) VALUES (?);", req.Data) +func Query(ctx context.Context, getRequestData GetRequestDataClient) (map[string]string, error) { + result, err := getRequestData(ctx) if err != nil { - return err + return nil, err } - return nil + + return map[string]string{"data": result[0].Data}, nil } diff --git a/backend/controller/sql/testdata/go/mysql/queries.ftl.go b/backend/controller/sql/testdata/go/mysql/queries.ftl.go index 477c5e9b6e..db03f365e6 100644 --- a/backend/controller/sql/testdata/go/mysql/queries.ftl.go +++ b/backend/controller/sql/testdata/go/mysql/queries.ftl.go @@ -2,28 +2,69 @@ package mysql import ( - "context" - "github.com/block/ftl/common/reflection" - // "github.com/block/ftl/go-runtime/server" + "context" + "github.com/alecthomas/types/tuple" + "github.com/block/ftl/common/reflection" + "github.com/block/ftl/go-runtime/ftl" + "github.com/block/ftl/go-runtime/server" + stdtime "time" ) type CreateRequestQuery struct { Data string } - -type CreateRequestClient func(context.Context, CreateRequestQuery) - +type GetAllTestTypesResult struct { + Id int + IntVal int + FloatVal float64 + TextVal string + BoolVal bool + TimeVal stdtime.Time + BlobVal []byte + OptionalVal ftl.Option[string] +} type GetRequestDataResult struct { Data string } - +type GetTestTypeQuery struct { + Id int +} +type GetTestTypeResult struct { + Id int + IntVal int + FloatVal float64 + TextVal string + BoolVal bool + TimeVal stdtime.Time + BlobVal []byte + OptionalVal ftl.Option[string] +} +type InsertTestTypesQuery struct { + IntVal int + FloatVal float64 + TextVal string + BoolVal bool + TimeVal stdtime.Time + BlobVal []byte + OptionalVal ftl.Option[string] +} + +type CreateRequestClient func(context.Context, CreateRequestQuery) error + +type GetAllTestTypesClient func(context.Context) ([]GetAllTestTypesResult, error) + type GetRequestDataClient func(context.Context) ([]GetRequestDataResult, error) + +type GetTestTypeClient func(context.Context, GetTestTypeQuery) (GetTestTypeResult, error) + +type InsertTestTypesClient func(context.Context, InsertTestTypesQuery) error func init() { reflection.Register( - // reflection.ProvideResourcesForVerb( - // server.Query[CreateRequestClient]("INSERT INTO requests (data) VALUES (?)"), - // server.Query[GetRequestDataClient]("SELECT data FROM requests"), - //), + server.QuerySink[CreateRequestQuery]("mysql", "createRequest", reflection.CommandTypeExec, "testdb", "INSERT INTO requests (data) VALUES (?)", []string{"Data"}, []tuple.Pair[string,string]{}), + server.QuerySource[GetAllTestTypesResult]("mysql", "getAllTestTypes", reflection.CommandTypeMany, "testdb", "SELECT id, int_val, float_val, text_val, bool_val, time_val, blob_val, optional_val FROM test_types", []string{}, []tuple.Pair[string,string]{tuple.PairOf("id", "Id"),tuple.PairOf("int_val", "IntVal"),tuple.PairOf("float_val", "FloatVal"),tuple.PairOf("text_val", "TextVal"),tuple.PairOf("bool_val", "BoolVal"),tuple.PairOf("time_val", "TimeVal"),tuple.PairOf("blob_val", "BlobVal"),tuple.PairOf("optional_val", "OptionalVal")}), + server.QuerySource[GetRequestDataResult]("mysql", "getRequestData", reflection.CommandTypeMany, "testdb", "SELECT data FROM requests", []string{}, []tuple.Pair[string,string]{tuple.PairOf("data", "Data")}), + server.Query[GetTestTypeQuery, GetTestTypeResult]("mysql", "getTestType", reflection.CommandTypeOne, "testdb", "SELECT id, int_val, float_val, text_val, bool_val, time_val, blob_val, optional_val FROM test_types WHERE id = ?", []string{"Id"}, []tuple.Pair[string,string]{tuple.PairOf("id", "Id"),tuple.PairOf("int_val", "IntVal"),tuple.PairOf("float_val", "FloatVal"),tuple.PairOf("text_val", "TextVal"),tuple.PairOf("bool_val", "BoolVal"),tuple.PairOf("time_val", "TimeVal"),tuple.PairOf("blob_val", "BlobVal"),tuple.PairOf("optional_val", "OptionalVal")}), + server.QuerySink[InsertTestTypesQuery]("mysql", "insertTestTypes", reflection.CommandTypeExec, "testdb", "INSERT INTO test_types (int_val, float_val, text_val, bool_val, time_val, blob_val, optional_val) VALUES (?, ?, ?, ?, ?, ?, ?)", []string{"IntVal","FloatVal","TextVal","BoolVal","TimeVal","BlobVal","OptionalVal"}, []tuple.Pair[string,string]{}), ) } \ No newline at end of file diff --git a/backend/controller/sql/testdata/go/mysql/types.ftl.go b/backend/controller/sql/testdata/go/mysql/types.ftl.go index 58033e0c7e..ced6a9cc1c 100644 --- a/backend/controller/sql/testdata/go/mysql/types.ftl.go +++ b/backend/controller/sql/testdata/go/mysql/types.ftl.go @@ -14,13 +14,15 @@ type QueryClient func(context.Context) (map[string]string, error) func init() { reflection.Register( reflection.Database[MyDbConfig]("testdb", server.InitMySQL), + reflection.ProvideResourcesForVerb( Insert, - server.DatabaseHandle[MyDbConfig]("mysql"), + server.SinkClient[CreateRequestClient, CreateRequestQuery](), ), + reflection.ProvideResourcesForVerb( Query, - server.DatabaseHandle[MyDbConfig]("mysql"), + server.SourceClient[GetRequestDataClient, []GetRequestDataResult](), ), ) } diff --git a/backend/protos/xyz/block/ftl/query/v1/query.pb.go b/backend/protos/xyz/block/ftl/query/v1/query.pb.go new file mode 100644 index 0000000000..461796434e --- /dev/null +++ b/backend/protos/xyz/block/ftl/query/v1/query.pb.go @@ -0,0 +1,1094 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: xyz/block/ftl/query/v1/query.proto + +package querypb + +import ( + v1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TransactionStatus int32 + +const ( + TransactionStatus_TRANSACTION_STATUS_UNSPECIFIED TransactionStatus = 0 + TransactionStatus_TRANSACTION_STATUS_SUCCESS TransactionStatus = 1 + TransactionStatus_TRANSACTION_STATUS_FAILED TransactionStatus = 2 +) + +// Enum value maps for TransactionStatus. +var ( + TransactionStatus_name = map[int32]string{ + 0: "TRANSACTION_STATUS_UNSPECIFIED", + 1: "TRANSACTION_STATUS_SUCCESS", + 2: "TRANSACTION_STATUS_FAILED", + } + TransactionStatus_value = map[string]int32{ + "TRANSACTION_STATUS_UNSPECIFIED": 0, + "TRANSACTION_STATUS_SUCCESS": 1, + "TRANSACTION_STATUS_FAILED": 2, + } +) + +func (x TransactionStatus) Enum() *TransactionStatus { + p := new(TransactionStatus) + *p = x + return p +} + +func (x TransactionStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransactionStatus) Descriptor() protoreflect.EnumDescriptor { + return file_xyz_block_ftl_query_v1_query_proto_enumTypes[0].Descriptor() +} + +func (TransactionStatus) Type() protoreflect.EnumType { + return &file_xyz_block_ftl_query_v1_query_proto_enumTypes[0] +} + +func (x TransactionStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransactionStatus.Descriptor instead. +func (TransactionStatus) EnumDescriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{0} +} + +type CommandType int32 + +const ( + CommandType_COMMAND_TYPE_UNSPECIFIED CommandType = 0 + CommandType_COMMAND_TYPE_EXEC CommandType = 1 + CommandType_COMMAND_TYPE_ONE CommandType = 2 + CommandType_COMMAND_TYPE_MANY CommandType = 3 +) + +// Enum value maps for CommandType. +var ( + CommandType_name = map[int32]string{ + 0: "COMMAND_TYPE_UNSPECIFIED", + 1: "COMMAND_TYPE_EXEC", + 2: "COMMAND_TYPE_ONE", + 3: "COMMAND_TYPE_MANY", + } + CommandType_value = map[string]int32{ + "COMMAND_TYPE_UNSPECIFIED": 0, + "COMMAND_TYPE_EXEC": 1, + "COMMAND_TYPE_ONE": 2, + "COMMAND_TYPE_MANY": 3, + } +) + +func (x CommandType) Enum() *CommandType { + p := new(CommandType) + *p = x + return p +} + +func (x CommandType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CommandType) Descriptor() protoreflect.EnumDescriptor { + return file_xyz_block_ftl_query_v1_query_proto_enumTypes[1].Descriptor() +} + +func (CommandType) Type() protoreflect.EnumType { + return &file_xyz_block_ftl_query_v1_query_proto_enumTypes[1] +} + +func (x CommandType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CommandType.Descriptor instead. +func (CommandType) EnumDescriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{1} +} + +type BeginTransactionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginTransactionRequest) Reset() { + *x = BeginTransactionRequest{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginTransactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginTransactionRequest) ProtoMessage() {} + +func (x *BeginTransactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginTransactionRequest.ProtoReflect.Descriptor instead. +func (*BeginTransactionRequest) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{0} +} + +type BeginTransactionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TransactionId string `protobuf:"bytes,1,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + Status TransactionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=xyz.block.ftl.query.v1.TransactionStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginTransactionResponse) Reset() { + *x = BeginTransactionResponse{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginTransactionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginTransactionResponse) ProtoMessage() {} + +func (x *BeginTransactionResponse) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BeginTransactionResponse.ProtoReflect.Descriptor instead. +func (*BeginTransactionResponse) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{1} +} + +func (x *BeginTransactionResponse) GetTransactionId() string { + if x != nil { + return x.TransactionId + } + return "" +} + +func (x *BeginTransactionResponse) GetStatus() TransactionStatus { + if x != nil { + return x.Status + } + return TransactionStatus_TRANSACTION_STATUS_UNSPECIFIED +} + +type CommitTransactionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TransactionId string `protobuf:"bytes,1,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitTransactionRequest) Reset() { + *x = CommitTransactionRequest{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitTransactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitTransactionRequest) ProtoMessage() {} + +func (x *CommitTransactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitTransactionRequest.ProtoReflect.Descriptor instead. +func (*CommitTransactionRequest) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{2} +} + +func (x *CommitTransactionRequest) GetTransactionId() string { + if x != nil { + return x.TransactionId + } + return "" +} + +type CommitTransactionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status TransactionStatus `protobuf:"varint,1,opt,name=status,proto3,enum=xyz.block.ftl.query.v1.TransactionStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommitTransactionResponse) Reset() { + *x = CommitTransactionResponse{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommitTransactionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitTransactionResponse) ProtoMessage() {} + +func (x *CommitTransactionResponse) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitTransactionResponse.ProtoReflect.Descriptor instead. +func (*CommitTransactionResponse) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{3} +} + +func (x *CommitTransactionResponse) GetStatus() TransactionStatus { + if x != nil { + return x.Status + } + return TransactionStatus_TRANSACTION_STATUS_UNSPECIFIED +} + +type RollbackTransactionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TransactionId string `protobuf:"bytes,1,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RollbackTransactionRequest) Reset() { + *x = RollbackTransactionRequest{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RollbackTransactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RollbackTransactionRequest) ProtoMessage() {} + +func (x *RollbackTransactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RollbackTransactionRequest.ProtoReflect.Descriptor instead. +func (*RollbackTransactionRequest) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{4} +} + +func (x *RollbackTransactionRequest) GetTransactionId() string { + if x != nil { + return x.TransactionId + } + return "" +} + +type RollbackTransactionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status TransactionStatus `protobuf:"varint,1,opt,name=status,proto3,enum=xyz.block.ftl.query.v1.TransactionStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RollbackTransactionResponse) Reset() { + *x = RollbackTransactionResponse{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RollbackTransactionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RollbackTransactionResponse) ProtoMessage() {} + +func (x *RollbackTransactionResponse) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RollbackTransactionResponse.ProtoReflect.Descriptor instead. +func (*RollbackTransactionResponse) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{5} +} + +func (x *RollbackTransactionResponse) GetStatus() TransactionStatus { + if x != nil { + return x.Status + } + return TransactionStatus_TRANSACTION_STATUS_UNSPECIFIED +} + +// A value that can be used as a SQL parameter +type SQLValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *SQLValue_StringValue + // *SQLValue_IntValue + // *SQLValue_FloatValue + // *SQLValue_BoolValue + // *SQLValue_BytesValue + // *SQLValue_TimestampValue + // *SQLValue_NullValue + Value isSQLValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SQLValue) Reset() { + *x = SQLValue{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SQLValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SQLValue) ProtoMessage() {} + +func (x *SQLValue) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SQLValue.ProtoReflect.Descriptor instead. +func (*SQLValue) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{6} +} + +func (x *SQLValue) GetValue() isSQLValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *SQLValue) GetStringValue() string { + if x != nil { + if x, ok := x.Value.(*SQLValue_StringValue); ok { + return x.StringValue + } + } + return "" +} + +func (x *SQLValue) GetIntValue() int64 { + if x != nil { + if x, ok := x.Value.(*SQLValue_IntValue); ok { + return x.IntValue + } + } + return 0 +} + +func (x *SQLValue) GetFloatValue() float64 { + if x != nil { + if x, ok := x.Value.(*SQLValue_FloatValue); ok { + return x.FloatValue + } + } + return 0 +} + +func (x *SQLValue) GetBoolValue() bool { + if x != nil { + if x, ok := x.Value.(*SQLValue_BoolValue); ok { + return x.BoolValue + } + } + return false +} + +func (x *SQLValue) GetBytesValue() []byte { + if x != nil { + if x, ok := x.Value.(*SQLValue_BytesValue); ok { + return x.BytesValue + } + } + return nil +} + +func (x *SQLValue) GetTimestampValue() *timestamppb.Timestamp { + if x != nil { + if x, ok := x.Value.(*SQLValue_TimestampValue); ok { + return x.TimestampValue + } + } + return nil +} + +func (x *SQLValue) GetNullValue() bool { + if x != nil { + if x, ok := x.Value.(*SQLValue_NullValue); ok { + return x.NullValue + } + } + return false +} + +type isSQLValue_Value interface { + isSQLValue_Value() +} + +type SQLValue_StringValue struct { + StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` +} + +type SQLValue_IntValue struct { + IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` +} + +type SQLValue_FloatValue struct { + FloatValue float64 `protobuf:"fixed64,3,opt,name=float_value,json=floatValue,proto3,oneof"` +} + +type SQLValue_BoolValue struct { + BoolValue bool `protobuf:"varint,4,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type SQLValue_BytesValue struct { + BytesValue []byte `protobuf:"bytes,5,opt,name=bytes_value,json=bytesValue,proto3,oneof"` +} + +type SQLValue_TimestampValue struct { + TimestampValue *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timestamp_value,json=timestampValue,proto3,oneof"` +} + +type SQLValue_NullValue struct { + NullValue bool `protobuf:"varint,7,opt,name=null_value,json=nullValue,proto3,oneof"` // Set to true to represent NULL +} + +func (*SQLValue_StringValue) isSQLValue_Value() {} + +func (*SQLValue_IntValue) isSQLValue_Value() {} + +func (*SQLValue_FloatValue) isSQLValue_Value() {} + +func (*SQLValue_BoolValue) isSQLValue_Value() {} + +func (*SQLValue_BytesValue) isSQLValue_Value() {} + +func (*SQLValue_TimestampValue) isSQLValue_Value() {} + +func (*SQLValue_NullValue) isSQLValue_Value() {} + +type ExecuteQueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RawSql string `protobuf:"bytes,1,opt,name=raw_sql,json=rawSql,proto3" json:"raw_sql,omitempty"` + CommandType CommandType `protobuf:"varint,2,opt,name=command_type,json=commandType,proto3,enum=xyz.block.ftl.query.v1.CommandType" json:"command_type,omitempty"` + Parameters []*SQLValue `protobuf:"bytes,3,rep,name=parameters,proto3" json:"parameters,omitempty"` // SQL parameter values in order + ResultColumns []string `protobuf:"bytes,6,rep,name=result_columns,json=resultColumns,proto3" json:"result_columns,omitempty"` // Column names to scan for the result type + TransactionId *string `protobuf:"bytes,4,opt,name=transaction_id,json=transactionId,proto3,oneof" json:"transaction_id,omitempty"` + BatchSize *int32 `protobuf:"varint,5,opt,name=batch_size,json=batchSize,proto3,oneof" json:"batch_size,omitempty"` // Default 100 if not set + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteQueryRequest) Reset() { + *x = ExecuteQueryRequest{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteQueryRequest) ProtoMessage() {} + +func (x *ExecuteQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteQueryRequest.ProtoReflect.Descriptor instead. +func (*ExecuteQueryRequest) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{7} +} + +func (x *ExecuteQueryRequest) GetRawSql() string { + if x != nil { + return x.RawSql + } + return "" +} + +func (x *ExecuteQueryRequest) GetCommandType() CommandType { + if x != nil { + return x.CommandType + } + return CommandType_COMMAND_TYPE_UNSPECIFIED +} + +func (x *ExecuteQueryRequest) GetParameters() []*SQLValue { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *ExecuteQueryRequest) GetResultColumns() []string { + if x != nil { + return x.ResultColumns + } + return nil +} + +func (x *ExecuteQueryRequest) GetTransactionId() string { + if x != nil && x.TransactionId != nil { + return *x.TransactionId + } + return "" +} + +func (x *ExecuteQueryRequest) GetBatchSize() int32 { + if x != nil && x.BatchSize != nil { + return *x.BatchSize + } + return 0 +} + +type ExecuteQueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *ExecuteQueryResponse_ExecResult + // *ExecuteQueryResponse_RowResults + Result isExecuteQueryResponse_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteQueryResponse) Reset() { + *x = ExecuteQueryResponse{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteQueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteQueryResponse) ProtoMessage() {} + +func (x *ExecuteQueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteQueryResponse.ProtoReflect.Descriptor instead. +func (*ExecuteQueryResponse) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{8} +} + +func (x *ExecuteQueryResponse) GetResult() isExecuteQueryResponse_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *ExecuteQueryResponse) GetExecResult() *ExecResult { + if x != nil { + if x, ok := x.Result.(*ExecuteQueryResponse_ExecResult); ok { + return x.ExecResult + } + } + return nil +} + +func (x *ExecuteQueryResponse) GetRowResults() *RowResults { + if x != nil { + if x, ok := x.Result.(*ExecuteQueryResponse_RowResults); ok { + return x.RowResults + } + } + return nil +} + +type isExecuteQueryResponse_Result interface { + isExecuteQueryResponse_Result() +} + +type ExecuteQueryResponse_ExecResult struct { + // For EXEC commands + ExecResult *ExecResult `protobuf:"bytes,1,opt,name=exec_result,json=execResult,proto3,oneof"` +} + +type ExecuteQueryResponse_RowResults struct { + // For ONE/MANY commands + RowResults *RowResults `protobuf:"bytes,2,opt,name=row_results,json=rowResults,proto3,oneof"` +} + +func (*ExecuteQueryResponse_ExecResult) isExecuteQueryResponse_Result() {} + +func (*ExecuteQueryResponse_RowResults) isExecuteQueryResponse_Result() {} + +type ExecResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + RowsAffected int64 `protobuf:"varint,1,opt,name=rows_affected,json=rowsAffected,proto3" json:"rows_affected,omitempty"` + LastInsertId *int64 `protobuf:"varint,2,opt,name=last_insert_id,json=lastInsertId,proto3,oneof" json:"last_insert_id,omitempty"` // Only for some databases like MySQL + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecResult) Reset() { + *x = ExecResult{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecResult) ProtoMessage() {} + +func (x *ExecResult) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecResult.ProtoReflect.Descriptor instead. +func (*ExecResult) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{9} +} + +func (x *ExecResult) GetRowsAffected() int64 { + if x != nil { + return x.RowsAffected + } + return 0 +} + +func (x *ExecResult) GetLastInsertId() int64 { + if x != nil && x.LastInsertId != nil { + return *x.LastInsertId + } + return 0 +} + +type RowResults struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rows map[string]*SQLValue `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Each row is a map of column name to value + HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` // Indicates if there are more rows to fetch + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RowResults) Reset() { + *x = RowResults{} + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RowResults) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RowResults) ProtoMessage() {} + +func (x *RowResults) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_query_v1_query_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RowResults.ProtoReflect.Descriptor instead. +func (*RowResults) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP(), []int{10} +} + +func (x *RowResults) GetRows() map[string]*SQLValue { + if x != nil { + return x.Rows + } + return nil +} + +func (x *RowResults) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +var File_xyz_block_ftl_query_v1_query_proto protoreflect.FileDescriptor + +var file_xyz_block_ftl_query_v1_query_proto_rawDesc = []byte{ + 0x0a, 0x22, 0x78, 0x79, 0x7a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x66, 0x74, 0x6c, 0x2f, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x31, 0x2f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, + 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1a, 0x78, + 0x79, 0x7a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x66, 0x74, 0x6c, 0x2f, 0x76, 0x31, 0x2f, + 0x66, 0x74, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x19, 0x0a, 0x17, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x84, 0x01, 0x0a, 0x18, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x41, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x41, 0x0a, 0x18, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x5e, + 0x0a, 0x19, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x78, 0x79, + 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x43, + 0x0a, 0x1a, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x22, 0x60, 0x0a, 0x1b, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, + 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xa6, 0x02, 0x0a, 0x08, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, + 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0b, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0a, 0x66, + 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, + 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, + 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0b, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x00, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, + 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x6e, 0x75, 0x6c, 0x6c, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd1, + 0x02, 0x0a, 0x13, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x61, 0x77, 0x5f, 0x73, 0x71, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x61, 0x77, 0x53, 0x71, 0x6c, 0x12, + 0x46, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x40, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x78, 0x79, + 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, + 0x12, 0x2a, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, + 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, + 0x48, 0x01, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, + 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x22, 0xae, 0x01, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x51, 0x75, + 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0b, 0x65, + 0x78, 0x65, 0x63, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, + 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x78, 0x65, 0x63, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x12, 0x45, 0x0a, 0x0b, 0x72, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x48, 0x00, 0x52, 0x0a, 0x72, + 0x6f, 0x77, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x22, 0x6f, 0x0a, 0x0a, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x6f, 0x77, 0x73, 0x5f, 0x61, 0x66, 0x66, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x6f, 0x77, 0x73, 0x41, 0x66, + 0x66, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, + 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x49, 0x64, 0x88, 0x01, + 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x65, 0x72, + 0x74, 0x5f, 0x69, 0x64, 0x22, 0xc4, 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x12, 0x40, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2c, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, + 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x77, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x52, 0x6f, 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, + 0x1a, 0x59, 0x0a, 0x09, 0x52, 0x6f, 0x77, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x36, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, + 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x76, 0x0a, 0x11, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x22, 0x0a, 0x1e, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, + 0x53, 0x53, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, + 0x44, 0x10, 0x02, 0x2a, 0x6f, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x45, 0x58, 0x45, 0x43, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4d, 0x4d, 0x41, + 0x4e, 0x44, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, + 0x11, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, + 0x4e, 0x59, 0x10, 0x03, 0x32, 0xb8, 0x04, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1d, 0x2e, + 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x78, + 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, + 0x01, 0x12, 0x75, 0x0a, 0x10, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x42, + 0x65, 0x67, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x42, 0x65, 0x67, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x11, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x2e, + 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x31, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x13, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x2e, 0x78, 0x79, 0x7a, 0x2e, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x0c, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x12, 0x2b, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, + 0x74, 0x6c, 0x2e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2c, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, + 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x2f, 0x66, 0x74, 0x6c, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x78, 0x79, 0x7a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x2f, 0x66, 0x74, 0x6c, 0x2f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_xyz_block_ftl_query_v1_query_proto_rawDescOnce sync.Once + file_xyz_block_ftl_query_v1_query_proto_rawDescData = file_xyz_block_ftl_query_v1_query_proto_rawDesc +) + +func file_xyz_block_ftl_query_v1_query_proto_rawDescGZIP() []byte { + file_xyz_block_ftl_query_v1_query_proto_rawDescOnce.Do(func() { + file_xyz_block_ftl_query_v1_query_proto_rawDescData = protoimpl.X.CompressGZIP(file_xyz_block_ftl_query_v1_query_proto_rawDescData) + }) + return file_xyz_block_ftl_query_v1_query_proto_rawDescData +} + +var file_xyz_block_ftl_query_v1_query_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_xyz_block_ftl_query_v1_query_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_xyz_block_ftl_query_v1_query_proto_goTypes = []any{ + (TransactionStatus)(0), // 0: xyz.block.ftl.query.v1.TransactionStatus + (CommandType)(0), // 1: xyz.block.ftl.query.v1.CommandType + (*BeginTransactionRequest)(nil), // 2: xyz.block.ftl.query.v1.BeginTransactionRequest + (*BeginTransactionResponse)(nil), // 3: xyz.block.ftl.query.v1.BeginTransactionResponse + (*CommitTransactionRequest)(nil), // 4: xyz.block.ftl.query.v1.CommitTransactionRequest + (*CommitTransactionResponse)(nil), // 5: xyz.block.ftl.query.v1.CommitTransactionResponse + (*RollbackTransactionRequest)(nil), // 6: xyz.block.ftl.query.v1.RollbackTransactionRequest + (*RollbackTransactionResponse)(nil), // 7: xyz.block.ftl.query.v1.RollbackTransactionResponse + (*SQLValue)(nil), // 8: xyz.block.ftl.query.v1.SQLValue + (*ExecuteQueryRequest)(nil), // 9: xyz.block.ftl.query.v1.ExecuteQueryRequest + (*ExecuteQueryResponse)(nil), // 10: xyz.block.ftl.query.v1.ExecuteQueryResponse + (*ExecResult)(nil), // 11: xyz.block.ftl.query.v1.ExecResult + (*RowResults)(nil), // 12: xyz.block.ftl.query.v1.RowResults + nil, // 13: xyz.block.ftl.query.v1.RowResults.RowsEntry + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp + (*v1.PingRequest)(nil), // 15: xyz.block.ftl.v1.PingRequest + (*v1.PingResponse)(nil), // 16: xyz.block.ftl.v1.PingResponse +} +var file_xyz_block_ftl_query_v1_query_proto_depIdxs = []int32{ + 0, // 0: xyz.block.ftl.query.v1.BeginTransactionResponse.status:type_name -> xyz.block.ftl.query.v1.TransactionStatus + 0, // 1: xyz.block.ftl.query.v1.CommitTransactionResponse.status:type_name -> xyz.block.ftl.query.v1.TransactionStatus + 0, // 2: xyz.block.ftl.query.v1.RollbackTransactionResponse.status:type_name -> xyz.block.ftl.query.v1.TransactionStatus + 14, // 3: xyz.block.ftl.query.v1.SQLValue.timestamp_value:type_name -> google.protobuf.Timestamp + 1, // 4: xyz.block.ftl.query.v1.ExecuteQueryRequest.command_type:type_name -> xyz.block.ftl.query.v1.CommandType + 8, // 5: xyz.block.ftl.query.v1.ExecuteQueryRequest.parameters:type_name -> xyz.block.ftl.query.v1.SQLValue + 11, // 6: xyz.block.ftl.query.v1.ExecuteQueryResponse.exec_result:type_name -> xyz.block.ftl.query.v1.ExecResult + 12, // 7: xyz.block.ftl.query.v1.ExecuteQueryResponse.row_results:type_name -> xyz.block.ftl.query.v1.RowResults + 13, // 8: xyz.block.ftl.query.v1.RowResults.rows:type_name -> xyz.block.ftl.query.v1.RowResults.RowsEntry + 8, // 9: xyz.block.ftl.query.v1.RowResults.RowsEntry.value:type_name -> xyz.block.ftl.query.v1.SQLValue + 15, // 10: xyz.block.ftl.query.v1.QueryService.Ping:input_type -> xyz.block.ftl.v1.PingRequest + 2, // 11: xyz.block.ftl.query.v1.QueryService.BeginTransaction:input_type -> xyz.block.ftl.query.v1.BeginTransactionRequest + 4, // 12: xyz.block.ftl.query.v1.QueryService.CommitTransaction:input_type -> xyz.block.ftl.query.v1.CommitTransactionRequest + 6, // 13: xyz.block.ftl.query.v1.QueryService.RollbackTransaction:input_type -> xyz.block.ftl.query.v1.RollbackTransactionRequest + 9, // 14: xyz.block.ftl.query.v1.QueryService.ExecuteQuery:input_type -> xyz.block.ftl.query.v1.ExecuteQueryRequest + 16, // 15: xyz.block.ftl.query.v1.QueryService.Ping:output_type -> xyz.block.ftl.v1.PingResponse + 3, // 16: xyz.block.ftl.query.v1.QueryService.BeginTransaction:output_type -> xyz.block.ftl.query.v1.BeginTransactionResponse + 5, // 17: xyz.block.ftl.query.v1.QueryService.CommitTransaction:output_type -> xyz.block.ftl.query.v1.CommitTransactionResponse + 7, // 18: xyz.block.ftl.query.v1.QueryService.RollbackTransaction:output_type -> xyz.block.ftl.query.v1.RollbackTransactionResponse + 10, // 19: xyz.block.ftl.query.v1.QueryService.ExecuteQuery:output_type -> xyz.block.ftl.query.v1.ExecuteQueryResponse + 15, // [15:20] is the sub-list for method output_type + 10, // [10:15] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_xyz_block_ftl_query_v1_query_proto_init() } +func file_xyz_block_ftl_query_v1_query_proto_init() { + if File_xyz_block_ftl_query_v1_query_proto != nil { + return + } + file_xyz_block_ftl_query_v1_query_proto_msgTypes[6].OneofWrappers = []any{ + (*SQLValue_StringValue)(nil), + (*SQLValue_IntValue)(nil), + (*SQLValue_FloatValue)(nil), + (*SQLValue_BoolValue)(nil), + (*SQLValue_BytesValue)(nil), + (*SQLValue_TimestampValue)(nil), + (*SQLValue_NullValue)(nil), + } + file_xyz_block_ftl_query_v1_query_proto_msgTypes[7].OneofWrappers = []any{} + file_xyz_block_ftl_query_v1_query_proto_msgTypes[8].OneofWrappers = []any{ + (*ExecuteQueryResponse_ExecResult)(nil), + (*ExecuteQueryResponse_RowResults)(nil), + } + file_xyz_block_ftl_query_v1_query_proto_msgTypes[9].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_xyz_block_ftl_query_v1_query_proto_rawDesc, + NumEnums: 2, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_xyz_block_ftl_query_v1_query_proto_goTypes, + DependencyIndexes: file_xyz_block_ftl_query_v1_query_proto_depIdxs, + EnumInfos: file_xyz_block_ftl_query_v1_query_proto_enumTypes, + MessageInfos: file_xyz_block_ftl_query_v1_query_proto_msgTypes, + }.Build() + File_xyz_block_ftl_query_v1_query_proto = out.File + file_xyz_block_ftl_query_v1_query_proto_rawDesc = nil + file_xyz_block_ftl_query_v1_query_proto_goTypes = nil + file_xyz_block_ftl_query_v1_query_proto_depIdxs = nil +} diff --git a/backend/protos/xyz/block/ftl/query/v1/query.proto b/backend/protos/xyz/block/ftl/query/v1/query.proto new file mode 100644 index 0000000000..dbd9ee555b --- /dev/null +++ b/backend/protos/xyz/block/ftl/query/v1/query.proto @@ -0,0 +1,104 @@ +syntax = "proto3"; + +package xyz.block.ftl.query.v1; + +import "google/protobuf/timestamp.proto"; +import "xyz/block/ftl/v1/ftl.proto"; + +option go_package = "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1;querypb"; + +message BeginTransactionRequest {} + +message BeginTransactionResponse { + string transaction_id = 1; + TransactionStatus status = 2; +} + +message CommitTransactionRequest { + string transaction_id = 1; +} + +message CommitTransactionResponse { + TransactionStatus status = 1; +} + +message RollbackTransactionRequest { + string transaction_id = 1; +} + +message RollbackTransactionResponse { + TransactionStatus status = 1; +} + +// A value that can be used as a SQL parameter +message SQLValue { + oneof value { + string string_value = 1; + int64 int_value = 2; + double float_value = 3; + bool bool_value = 4; + bytes bytes_value = 5; + google.protobuf.Timestamp timestamp_value = 6; + bool null_value = 7; // Set to true to represent NULL + } +} + +message ExecuteQueryRequest { + string raw_sql = 1; + CommandType command_type = 2; + repeated SQLValue parameters = 3; // SQL parameter values in order + repeated string result_columns = 6; // Column names to scan for the result type + optional string transaction_id = 4; + optional int32 batch_size = 5; // Default 100 if not set +} + +message ExecuteQueryResponse { + oneof result { + // For EXEC commands + ExecResult exec_result = 1; + // For ONE/MANY commands + RowResults row_results = 2; + } +} + +message ExecResult { + int64 rows_affected = 1; + optional int64 last_insert_id = 2; // Only for some databases like MySQL +} + +message RowResults { + map rows = 1; // Each row is a map of column name to value + bool has_more = 2; // Indicates if there are more rows to fetch +} + +enum TransactionStatus { + TRANSACTION_STATUS_UNSPECIFIED = 0; + TRANSACTION_STATUS_SUCCESS = 1; + TRANSACTION_STATUS_FAILED = 2; +} + +enum CommandType { + COMMAND_TYPE_UNSPECIFIED = 0; + COMMAND_TYPE_EXEC = 1; + COMMAND_TYPE_ONE = 2; + COMMAND_TYPE_MANY = 3; +} + +service QueryService { + // Ping service for readiness + rpc Ping(ftl.v1.PingRequest) returns (ftl.v1.PingResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } + + // Begins a new transaction and returns a transaction ID. + rpc BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse); + + // Commits a transaction. + rpc CommitTransaction(CommitTransactionRequest) returns (CommitTransactionResponse); + + // Rolls back a transaction. + rpc RollbackTransaction(RollbackTransactionRequest) returns (RollbackTransactionResponse); + + // Executes a raw SQL query, optionally within a transaction. + rpc ExecuteQuery(ExecuteQueryRequest) returns (stream ExecuteQueryResponse); +} diff --git a/backend/protos/xyz/block/ftl/query/v1/querypbconnect/query.connect.go b/backend/protos/xyz/block/ftl/query/v1/querypbconnect/query.connect.go new file mode 100644 index 0000000000..b012e717e3 --- /dev/null +++ b/backend/protos/xyz/block/ftl/query/v1/querypbconnect/query.connect.go @@ -0,0 +1,225 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: xyz/block/ftl/query/v1/query.proto + +package querypbconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v11 "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1" + v1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_7_0 + +const ( + // QueryServiceName is the fully-qualified name of the QueryService service. + QueryServiceName = "xyz.block.ftl.query.v1.QueryService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // QueryServicePingProcedure is the fully-qualified name of the QueryService's Ping RPC. + QueryServicePingProcedure = "/xyz.block.ftl.query.v1.QueryService/Ping" + // QueryServiceBeginTransactionProcedure is the fully-qualified name of the QueryService's + // BeginTransaction RPC. + QueryServiceBeginTransactionProcedure = "/xyz.block.ftl.query.v1.QueryService/BeginTransaction" + // QueryServiceCommitTransactionProcedure is the fully-qualified name of the QueryService's + // CommitTransaction RPC. + QueryServiceCommitTransactionProcedure = "/xyz.block.ftl.query.v1.QueryService/CommitTransaction" + // QueryServiceRollbackTransactionProcedure is the fully-qualified name of the QueryService's + // RollbackTransaction RPC. + QueryServiceRollbackTransactionProcedure = "/xyz.block.ftl.query.v1.QueryService/RollbackTransaction" + // QueryServiceExecuteQueryProcedure is the fully-qualified name of the QueryService's ExecuteQuery + // RPC. + QueryServiceExecuteQueryProcedure = "/xyz.block.ftl.query.v1.QueryService/ExecuteQuery" +) + +// QueryServiceClient is a client for the xyz.block.ftl.query.v1.QueryService service. +type QueryServiceClient interface { + // Ping service for readiness + Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) + // Begins a new transaction and returns a transaction ID. + BeginTransaction(context.Context, *connect.Request[v11.BeginTransactionRequest]) (*connect.Response[v11.BeginTransactionResponse], error) + // Commits a transaction. + CommitTransaction(context.Context, *connect.Request[v11.CommitTransactionRequest]) (*connect.Response[v11.CommitTransactionResponse], error) + // Rolls back a transaction. + RollbackTransaction(context.Context, *connect.Request[v11.RollbackTransactionRequest]) (*connect.Response[v11.RollbackTransactionResponse], error) + // Executes a raw SQL query, optionally within a transaction. + ExecuteQuery(context.Context, *connect.Request[v11.ExecuteQueryRequest]) (*connect.ServerStreamForClient[v11.ExecuteQueryResponse], error) +} + +// NewQueryServiceClient constructs a client for the xyz.block.ftl.query.v1.QueryService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewQueryServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) QueryServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &queryServiceClient{ + ping: connect.NewClient[v1.PingRequest, v1.PingResponse]( + httpClient, + baseURL+QueryServicePingProcedure, + connect.WithIdempotency(connect.IdempotencyNoSideEffects), + connect.WithClientOptions(opts...), + ), + beginTransaction: connect.NewClient[v11.BeginTransactionRequest, v11.BeginTransactionResponse]( + httpClient, + baseURL+QueryServiceBeginTransactionProcedure, + opts..., + ), + commitTransaction: connect.NewClient[v11.CommitTransactionRequest, v11.CommitTransactionResponse]( + httpClient, + baseURL+QueryServiceCommitTransactionProcedure, + opts..., + ), + rollbackTransaction: connect.NewClient[v11.RollbackTransactionRequest, v11.RollbackTransactionResponse]( + httpClient, + baseURL+QueryServiceRollbackTransactionProcedure, + opts..., + ), + executeQuery: connect.NewClient[v11.ExecuteQueryRequest, v11.ExecuteQueryResponse]( + httpClient, + baseURL+QueryServiceExecuteQueryProcedure, + opts..., + ), + } +} + +// queryServiceClient implements QueryServiceClient. +type queryServiceClient struct { + ping *connect.Client[v1.PingRequest, v1.PingResponse] + beginTransaction *connect.Client[v11.BeginTransactionRequest, v11.BeginTransactionResponse] + commitTransaction *connect.Client[v11.CommitTransactionRequest, v11.CommitTransactionResponse] + rollbackTransaction *connect.Client[v11.RollbackTransactionRequest, v11.RollbackTransactionResponse] + executeQuery *connect.Client[v11.ExecuteQueryRequest, v11.ExecuteQueryResponse] +} + +// Ping calls xyz.block.ftl.query.v1.QueryService.Ping. +func (c *queryServiceClient) Ping(ctx context.Context, req *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { + return c.ping.CallUnary(ctx, req) +} + +// BeginTransaction calls xyz.block.ftl.query.v1.QueryService.BeginTransaction. +func (c *queryServiceClient) BeginTransaction(ctx context.Context, req *connect.Request[v11.BeginTransactionRequest]) (*connect.Response[v11.BeginTransactionResponse], error) { + return c.beginTransaction.CallUnary(ctx, req) +} + +// CommitTransaction calls xyz.block.ftl.query.v1.QueryService.CommitTransaction. +func (c *queryServiceClient) CommitTransaction(ctx context.Context, req *connect.Request[v11.CommitTransactionRequest]) (*connect.Response[v11.CommitTransactionResponse], error) { + return c.commitTransaction.CallUnary(ctx, req) +} + +// RollbackTransaction calls xyz.block.ftl.query.v1.QueryService.RollbackTransaction. +func (c *queryServiceClient) RollbackTransaction(ctx context.Context, req *connect.Request[v11.RollbackTransactionRequest]) (*connect.Response[v11.RollbackTransactionResponse], error) { + return c.rollbackTransaction.CallUnary(ctx, req) +} + +// ExecuteQuery calls xyz.block.ftl.query.v1.QueryService.ExecuteQuery. +func (c *queryServiceClient) ExecuteQuery(ctx context.Context, req *connect.Request[v11.ExecuteQueryRequest]) (*connect.ServerStreamForClient[v11.ExecuteQueryResponse], error) { + return c.executeQuery.CallServerStream(ctx, req) +} + +// QueryServiceHandler is an implementation of the xyz.block.ftl.query.v1.QueryService service. +type QueryServiceHandler interface { + // Ping service for readiness + Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) + // Begins a new transaction and returns a transaction ID. + BeginTransaction(context.Context, *connect.Request[v11.BeginTransactionRequest]) (*connect.Response[v11.BeginTransactionResponse], error) + // Commits a transaction. + CommitTransaction(context.Context, *connect.Request[v11.CommitTransactionRequest]) (*connect.Response[v11.CommitTransactionResponse], error) + // Rolls back a transaction. + RollbackTransaction(context.Context, *connect.Request[v11.RollbackTransactionRequest]) (*connect.Response[v11.RollbackTransactionResponse], error) + // Executes a raw SQL query, optionally within a transaction. + ExecuteQuery(context.Context, *connect.Request[v11.ExecuteQueryRequest], *connect.ServerStream[v11.ExecuteQueryResponse]) error +} + +// NewQueryServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewQueryServiceHandler(svc QueryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + queryServicePingHandler := connect.NewUnaryHandler( + QueryServicePingProcedure, + svc.Ping, + connect.WithIdempotency(connect.IdempotencyNoSideEffects), + connect.WithHandlerOptions(opts...), + ) + queryServiceBeginTransactionHandler := connect.NewUnaryHandler( + QueryServiceBeginTransactionProcedure, + svc.BeginTransaction, + opts..., + ) + queryServiceCommitTransactionHandler := connect.NewUnaryHandler( + QueryServiceCommitTransactionProcedure, + svc.CommitTransaction, + opts..., + ) + queryServiceRollbackTransactionHandler := connect.NewUnaryHandler( + QueryServiceRollbackTransactionProcedure, + svc.RollbackTransaction, + opts..., + ) + queryServiceExecuteQueryHandler := connect.NewServerStreamHandler( + QueryServiceExecuteQueryProcedure, + svc.ExecuteQuery, + opts..., + ) + return "/xyz.block.ftl.query.v1.QueryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case QueryServicePingProcedure: + queryServicePingHandler.ServeHTTP(w, r) + case QueryServiceBeginTransactionProcedure: + queryServiceBeginTransactionHandler.ServeHTTP(w, r) + case QueryServiceCommitTransactionProcedure: + queryServiceCommitTransactionHandler.ServeHTTP(w, r) + case QueryServiceRollbackTransactionProcedure: + queryServiceRollbackTransactionHandler.ServeHTTP(w, r) + case QueryServiceExecuteQueryProcedure: + queryServiceExecuteQueryHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedQueryServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedQueryServiceHandler struct{} + +func (UnimplementedQueryServiceHandler) Ping(context.Context, *connect.Request[v1.PingRequest]) (*connect.Response[v1.PingResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.query.v1.QueryService.Ping is not implemented")) +} + +func (UnimplementedQueryServiceHandler) BeginTransaction(context.Context, *connect.Request[v11.BeginTransactionRequest]) (*connect.Response[v11.BeginTransactionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.query.v1.QueryService.BeginTransaction is not implemented")) +} + +func (UnimplementedQueryServiceHandler) CommitTransaction(context.Context, *connect.Request[v11.CommitTransactionRequest]) (*connect.Response[v11.CommitTransactionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.query.v1.QueryService.CommitTransaction is not implemented")) +} + +func (UnimplementedQueryServiceHandler) RollbackTransaction(context.Context, *connect.Request[v11.RollbackTransactionRequest]) (*connect.Response[v11.RollbackTransactionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.query.v1.QueryService.RollbackTransaction is not implemented")) +} + +func (UnimplementedQueryServiceHandler) ExecuteQuery(context.Context, *connect.Request[v11.ExecuteQueryRequest], *connect.ServerStream[v11.ExecuteQueryResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.query.v1.QueryService.ExecuteQuery is not implemented")) +} diff --git a/backend/runner/proxy/proxy.go b/backend/runner/proxy/proxy.go index 7acc517dcd..2c70e60d02 100644 --- a/backend/runner/proxy/proxy.go +++ b/backend/runner/proxy/proxy.go @@ -17,6 +17,7 @@ import ( ftlleaseconnect "github.com/block/ftl/backend/protos/xyz/block/ftl/lease/v1/leasepbconnect" ftlv1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1" "github.com/block/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" + "github.com/block/ftl/backend/runner/query" "github.com/block/ftl/common/schema" "github.com/block/ftl/internal/key" "github.com/block/ftl/internal/log" @@ -38,14 +39,16 @@ type Service struct { controllerLeaseService ftlleaseconnect.LeaseServiceClient moduleVerbService *xsync.MapOf[string, moduleVerbService] timelineClient *timelineclient.Client + queryService *query.MultiService } -func New(controllerModuleService ftldeploymentconnect.DeploymentServiceClient, leaseClient ftlleaseconnect.LeaseServiceClient, timelineClient *timelineclient.Client) *Service { +func New(controllerModuleService ftldeploymentconnect.DeploymentServiceClient, leaseClient ftlleaseconnect.LeaseServiceClient, timelineClient *timelineclient.Client, queryService *query.MultiService) *Service { proxy := &Service{ controllerDeploymentService: controllerModuleService, controllerLeaseService: leaseClient, moduleVerbService: xsync.NewMapOf[string, moduleVerbService](), timelineClient: timelineClient, + queryService: queryService, } return proxy } diff --git a/backend/runner/pubsub/testdata/go/publisher/go.mod b/backend/runner/pubsub/testdata/go/publisher/go.mod index d70e0ae4e8..e6d20f75e7 100644 --- a/backend/runner/pubsub/testdata/go/publisher/go.mod +++ b/backend/runner/pubsub/testdata/go/publisher/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/backend/runner/pubsub/testdata/go/publisher/go.sum b/backend/runner/pubsub/testdata/go/publisher/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/backend/runner/pubsub/testdata/go/publisher/go.sum +++ b/backend/runner/pubsub/testdata/go/publisher/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/backend/runner/query/client.go b/backend/runner/query/client.go new file mode 100644 index 0000000000..e8d78c8f59 --- /dev/null +++ b/backend/runner/query/client.go @@ -0,0 +1,311 @@ +package query + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + "time" + + "connectrpc.com/connect" + "github.com/alecthomas/types/tuple" + querypb "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1" + queryconnect "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1/querypbconnect" + "github.com/block/ftl/internal/log" + "github.com/block/ftl/internal/rpc" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// SQLNullable is an interface that allows for setting a value to null/none. Optional types implement this interface. +type SQLNullable interface { + // SetNull sets the value to null/none + SetNull() + // SetValue sets the underlying value + SetValue(value reflect.Value) error + // GetType returns the type of the value in the Option + GetType() reflect.Type +} + +func One[Req, Resp any](ctx context.Context, dbName string, rawSQL string, params []any, colFieldNames []tuple.Pair[string, string]) (resp Resp, err error) { + client, err := newClient(dbName) + if err != nil { + return resp, err + } + respType := reflect.TypeFor[Resp]() + results, err := client.performQuery(ctx, respType, querypb.CommandType_COMMAND_TYPE_ONE, rawSQL, params, colFieldNames) + if err != nil { + return resp, err + } + if len(results) == 0 { + return resp, fmt.Errorf("no results found") + } + return results[0].(Resp), nil //nolint:forcetypeassert +} + +func Many[Req, Resp any](ctx context.Context, dbName string, rawSQL string, params []any, colFieldNames []tuple.Pair[string, string]) ([]Resp, error) { + client, err := newClient(dbName) + if err != nil { + return nil, err + } + respType := reflect.TypeFor[Resp]() + results, err := client.performQuery(ctx, respType, querypb.CommandType_COMMAND_TYPE_MANY, rawSQL, params, colFieldNames) + if err != nil { + return nil, err + } + typed := make([]Resp, len(results)) + for i, r := range results { + typed[i] = r.(Resp) //nolint:forcetypeassert + } + return typed, nil +} + +func Exec[Req any](ctx context.Context, dbName string, rawSQL string, params []any, colFieldNames []tuple.Pair[string, string]) error { + client, err := newClient(dbName) + if err != nil { + return err + } + _, err = client.performQuery(ctx, nil, querypb.CommandType_COMMAND_TYPE_EXEC, rawSQL, params, colFieldNames) + return err +} + +type client struct { + queryconnect.QueryServiceClient +} + +func newClient(dbName string) (*client, error) { + address := os.Getenv(strings.ToUpper("FTL_QUERY_ADDRESS_" + dbName)) + if address == "" { + return nil, fmt.Errorf("query address for %s not found", dbName) + } + return &client{QueryServiceClient: rpc.Dial(queryconnect.NewQueryServiceClient, address, log.Error)}, nil +} + +// performQuery performs a SQL query and returns the results. +// +// note: accepts colFieldNames as []tuple.Pair[string, string] rather than map[string]string to preserve column order +func (c *client) performQuery(ctx context.Context, respType reflect.Type, commandType querypb.CommandType, rawSQL string, params []any, colFieldNames []tuple.Pair[string, string]) ([]any, error) { + sqlParams := make([]*querypb.SQLValue, 0, len(params)) + for _, param := range params { + sqlValue, err := convertParamToSQLValue(param) + if err != nil { + return nil, fmt.Errorf("failed to convert parameter %v of type %T: %w", param, param, err) + } + sqlParams = append(sqlParams, sqlValue) + } + + colToFieldName := map[string]string{} + for _, pair := range colFieldNames { + colToFieldName[pair.A] = pair.B + } + + resultCols := make([]string, 0, len(colFieldNames)) + for _, pair := range colFieldNames { + resultCols = append(resultCols, pair.A) + } + + stream, err := c.QueryServiceClient.ExecuteQuery(ctx, connect.NewRequest(&querypb.ExecuteQueryRequest{ + RawSql: rawSQL, + CommandType: commandType, + Parameters: sqlParams, + ResultColumns: resultCols, + })) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + var results []any + for stream.Receive() { + resp := stream.Msg() + switch r := resp.Result.(type) { + case *querypb.ExecuteQueryResponse_ExecResult: + return nil, nil + case *querypb.ExecuteQueryResponse_RowResults: + if len(r.RowResults.Rows) == 0 { + continue + } + + respValue := reflect.New(respType).Elem() + for col, field := range colToFieldName { + if sqlVal, ok := r.RowResults.Rows[col]; ok { + fieldValue := respValue.FieldByName(field) + if !fieldValue.IsValid() { + continue + } + if err := setFieldFromSQLValue(fieldValue, sqlVal); err != nil { + return nil, fmt.Errorf("failed to set field %s from column %s: value type %T = %v: %w", + field, col, sqlVal.Value, sqlVal.Value, err) + } + } + } + results = append(results, respValue.Interface()) + } + } + if err := stream.Err(); err != nil { + return nil, fmt.Errorf("failed to receive query results: %w", err) + } + return results, nil +} + +func setFieldFromSQLValue(field reflect.Value, sqlValue *querypb.SQLValue) error { + if !field.CanSet() { + return fmt.Errorf("field is not settable") + } + + var nullable SQLNullable + var ok bool + if field.Kind() == reflect.Ptr { + nullable, ok = field.Interface().(SQLNullable) + } else if field.CanAddr() { + nullable, ok = field.Addr().Interface().(SQLNullable) + } + + // handle null values + if sqlValue == nil || sqlValue.Value == nil { + if ok { + nullable.SetNull() + return nil + } + return nil + } + if _, isNull := sqlValue.Value.(*querypb.SQLValue_NullValue); isNull { + if ok { + nullable.SetNull() + return nil + } + return nil + } + + // handle non-null values + if ok { + var newValue reflect.Value + if field.Kind() == reflect.Ptr { + newValue = reflect.New(field.Type().Elem()) + if err := setNonNullValue(newValue.Elem(), sqlValue); err != nil { + return err + } + } else { + newValue = reflect.New(nullable.GetType()).Elem() + if err := setNonNullValue(newValue, sqlValue); err != nil { + return err + } + return nil + } + err := nullable.SetValue(newValue) + if err != nil { + return fmt.Errorf("failed to set value: %w", err) + } + return nil + } + return setNonNullValue(field, sqlValue) +} + +func setNonNullValue(field reflect.Value, sqlValue *querypb.SQLValue) error { + switch v := sqlValue.Value.(type) { + case *querypb.SQLValue_StringValue: + if field.Kind() != reflect.String { + return fmt.Errorf("cannot convert string value %q to field type %s", v.StringValue, field.Type()) + } + field.SetString(v.StringValue) + + case *querypb.SQLValue_IntValue: + switch field.Kind() { + case reflect.Int, reflect.Int64, reflect.Int32: + field.SetInt(v.IntValue) + case reflect.Float64, reflect.Float32: + field.SetFloat(float64(v.IntValue)) + case reflect.Bool: + field.SetBool(v.IntValue != 0) + case reflect.Struct: + if field.Type() == reflect.TypeOf(time.Time{}) { + field.Set(reflect.ValueOf(time.Unix(v.IntValue, 0))) + } else { + return fmt.Errorf("cannot convert int value %d to field type %s", v.IntValue, field.Type()) + } + default: + return fmt.Errorf("cannot convert int value %d to field type %s", v.IntValue, field.Type()) + } + + case *querypb.SQLValue_FloatValue: + switch field.Kind() { + case reflect.Float64, reflect.Float32: + field.SetFloat(v.FloatValue) + case reflect.Int, reflect.Int64, reflect.Int32: + field.SetInt(int64(v.FloatValue)) + case reflect.Bool: + field.SetBool(v.FloatValue != 0) + default: + return fmt.Errorf("cannot convert float value %f to field type %s", v.FloatValue, field.Type()) + } + + case *querypb.SQLValue_BoolValue: + if field.Kind() != reflect.Bool { + return fmt.Errorf("cannot convert bool value %v to field type %s", v.BoolValue, field.Type()) + } + field.SetBool(v.BoolValue) + + case *querypb.SQLValue_BytesValue: + str := string(v.BytesValue) + switch field.Kind() { + case reflect.String: + field.SetString(str) + case reflect.Slice: + if field.Type().Elem().Kind() == reflect.Uint8 { + field.SetBytes(v.BytesValue) + } else { + return fmt.Errorf("cannot convert bytes value to field type %s", field.Type()) + } + case reflect.Struct: + if field.Type() == reflect.TypeOf(time.Time{}) { + for _, format := range []string{ + "2006-01-02 15:04:05", + "2006-01-02 15:04:05.000000", + time.RFC3339, + time.RFC3339Nano, + } { + if t, err := time.Parse(format, str); err == nil { + field.Set(reflect.ValueOf(t)) + return nil + } + } + return fmt.Errorf("cannot parse time string %q", str) + } + return fmt.Errorf("cannot convert bytes value to field type %s", field.Type()) + default: + return fmt.Errorf("cannot convert bytes value to field type %s", field.Type()) + } + + case *querypb.SQLValue_TimestampValue: + if field.Type() != reflect.TypeOf(time.Time{}) { + return fmt.Errorf("cannot convert timestamp value %v to field type %s", v.TimestampValue.AsTime(), field.Type()) + } + field.Set(reflect.ValueOf(v.TimestampValue.AsTime())) + + default: + return fmt.Errorf("unsupported SQLValue type: %T", v) + } + return nil +} + +func convertParamToSQLValue(v any) (*querypb.SQLValue, error) { + if v == nil { + return &querypb.SQLValue{Value: &querypb.SQLValue_NullValue{NullValue: true}}, nil + } + + switch val := v.(type) { + case string: + return &querypb.SQLValue{Value: &querypb.SQLValue_StringValue{StringValue: val}}, nil + case int, int32, int64: + return &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: reflect.ValueOf(val).Int()}}, nil + case float32, float64: + return &querypb.SQLValue{Value: &querypb.SQLValue_FloatValue{FloatValue: reflect.ValueOf(val).Float()}}, nil + case bool: + return &querypb.SQLValue{Value: &querypb.SQLValue_BoolValue{BoolValue: val}}, nil + case []byte: + return &querypb.SQLValue{Value: &querypb.SQLValue_BytesValue{BytesValue: val}}, nil + case time.Time: + return &querypb.SQLValue{Value: &querypb.SQLValue_TimestampValue{TimestampValue: timestamppb.New(val)}}, nil + default: + return nil, fmt.Errorf("unsupported type: %T", v) + } +} diff --git a/backend/runner/query/service.go b/backend/runner/query/service.go new file mode 100644 index 0000000000..7c859a3161 --- /dev/null +++ b/backend/runner/query/service.go @@ -0,0 +1,458 @@ +package query + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/url" + "os" + "strings" + "sync" + "time" + + "connectrpc.com/connect" + _ "github.com/go-sql-driver/mysql" + "github.com/google/uuid" + _ "github.com/lib/pq" + "github.com/puzpuzpuz/xsync/v3" + "google.golang.org/protobuf/types/known/timestamppb" + + querypb "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1" + queryconnect "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1/querypbconnect" + ftlv1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1" + "github.com/block/ftl/common/schema" + "github.com/block/ftl/internal/log" +) + +// MultiService handles multiple database instances +type MultiService struct { + services *xsync.MapOf[schema.RefKey, *Service] +} + +func NewMultiService() *MultiService { + return &MultiService{services: xsync.NewMapOf[schema.RefKey, *Service]()} +} + +func (m *MultiService) AddService(module string, name string, svc *Service) error { + refKey := schema.RefKey{Module: module, Name: name} + m.services.Store(refKey, svc) + envVar := strings.ToUpper("FTL_QUERY_ADDRESS_" + name) + if err := os.Setenv(envVar, svc.endpoint.String()); err != nil { + return fmt.Errorf("failed to set query address for %s: %w", name, err) + } + return nil +} + +func (m *MultiService) GetServices() map[schema.RefKey]*Service { + result := make(map[schema.RefKey]*Service) + m.services.Range(func(key schema.RefKey, value *Service) bool { + result[key] = value + return true + }) + return result +} + +func (m *MultiService) Close() error { + var lastErr error + m.services.Range(func(key schema.RefKey, svc *Service) bool { + if err := svc.Close(); err != nil { + lastErr = err + } + return true + }) + return lastErr +} + +func (m *MultiService) GetService(ref schema.RefKey) (*Service, bool) { + return m.services.Load(ref) +} + +type Config struct { + DSN string + Engine string + Endpoint *url.URL +} + +func (c *Config) Validate() error { + if c.DSN == "" { + return fmt.Errorf("database connection string is required") + } + if c.Endpoint == nil { + return fmt.Errorf("service endpoint is required") + } + switch c.Engine { + case "mysql", "postgres": + return nil + default: + return fmt.Errorf("unsupported database engine: %s (supported: mysql, postgres)", c.Engine) + } +} + +type Service struct { + endpoint *url.URL + lock sync.RWMutex + transactions map[string]*sql.Tx + db *sql.DB + engine string +} + +// DB represents a database that can execute queries +type DB interface { + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) +} + +func New(ctx context.Context, config Config) (*Service, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + logger := log.FromContext(ctx).Scope("query") + ctx = log.ContextWithLogger(ctx, logger) + + db, err := sql.Open(getDriverName(config.Engine), config.DSN) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + return &Service{ + endpoint: config.Endpoint, + transactions: make(map[string]*sql.Tx), + db: db, + engine: config.Engine, + }, nil +} + +func (s *Service) Close() error { + if s.db != nil { + s.db.Close() + } + return nil +} + +var _ queryconnect.QueryServiceHandler = (*Service)(nil) + +func (s *Service) Ping(ctx context.Context, req *connect.Request[ftlv1.PingRequest]) (*connect.Response[ftlv1.PingResponse], error) { + return connect.NewResponse(&ftlv1.PingResponse{}), nil +} + +func (s *Service) BeginTransaction(ctx context.Context, req *connect.Request[querypb.BeginTransactionRequest]) (*connect.Response[querypb.BeginTransactionResponse], error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to begin transaction: %w", err)) + } + + transactionID := uuid.New().String() + + s.lock.Lock() + s.transactions[transactionID] = tx + s.lock.Unlock() + + return connect.NewResponse(&querypb.BeginTransactionResponse{ + TransactionId: transactionID, + Status: querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, + }), nil +} + +func (s *Service) CommitTransaction(ctx context.Context, req *connect.Request[querypb.CommitTransactionRequest]) (*connect.Response[querypb.CommitTransactionResponse], error) { + s.lock.Lock() + tx, exists := s.transactions[req.Msg.TransactionId] + if !exists { + s.lock.Unlock() + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("transaction %s not found", req.Msg.TransactionId)) + } + delete(s.transactions, req.Msg.TransactionId) + s.lock.Unlock() + + if err := tx.Commit(); err != nil { + return connect.NewResponse(&querypb.CommitTransactionResponse{ + Status: querypb.TransactionStatus_TRANSACTION_STATUS_FAILED, + }), nil + } + + return connect.NewResponse(&querypb.CommitTransactionResponse{ + Status: querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, + }), nil +} + +func (s *Service) RollbackTransaction(ctx context.Context, req *connect.Request[querypb.RollbackTransactionRequest]) (*connect.Response[querypb.RollbackTransactionResponse], error) { + s.lock.Lock() + tx, exists := s.transactions[req.Msg.TransactionId] + if !exists { + s.lock.Unlock() + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("transaction %s not found", req.Msg.TransactionId)) + } + delete(s.transactions, req.Msg.TransactionId) + s.lock.Unlock() + + if err := tx.Rollback(); err != nil { + return connect.NewResponse(&querypb.RollbackTransactionResponse{ + Status: querypb.TransactionStatus_TRANSACTION_STATUS_FAILED, + }), nil + } + + return connect.NewResponse(&querypb.RollbackTransactionResponse{ + Status: querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, + }), nil +} + +func (s *Service) ExecuteQuery(ctx context.Context, req *connect.Request[querypb.ExecuteQueryRequest], stream *connect.ServerStream[querypb.ExecuteQueryResponse]) error { + if req.Msg.TransactionId != nil && *req.Msg.TransactionId != "" { + s.lock.RLock() + tx, ok := s.transactions[*req.Msg.TransactionId] + s.lock.RUnlock() + if !ok { + return connect.NewError(connect.CodeNotFound, fmt.Errorf("transaction not found: %s", *req.Msg.TransactionId)) + } + return s.executeQuery(ctx, tx, req.Msg, stream) + } + return s.executeQuery(ctx, s.db, req.Msg, stream) +} + +// SetEndpoint sets the endpoint after the server is confirmed ready +func (s *Service) SetEndpoint(endpoint *url.URL) { + s.endpoint = endpoint +} + +func (s *Service) executeQuery(ctx context.Context, db DB, req *querypb.ExecuteQueryRequest, stream *connect.ServerStream[querypb.ExecuteQueryResponse]) error { + switch req.CommandType { + case querypb.CommandType_COMMAND_TYPE_EXEC: + result, err := db.ExecContext(ctx, req.RawSql, convertParameters(req.Parameters)...) + if err != nil { + return connect.NewError(connect.CodeInternal, fmt.Errorf("failed to execute query: %w", err)) + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get rows affected: %w", err)) + } + lastInsertID, err := result.LastInsertId() + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get last insert id: %w", err)) + } + err = stream.Send(&querypb.ExecuteQueryResponse{ + Result: &querypb.ExecuteQueryResponse_ExecResult{ + ExecResult: &querypb.ExecResult{ + RowsAffected: rowsAffected, + LastInsertId: &lastInsertID, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to send exec result: %w", err) + } + + case querypb.CommandType_COMMAND_TYPE_ONE: + row := db.QueryRowContext(ctx, req.RawSql, convertParameters(req.Parameters)...) + result, err := scanRowToMap(row, req.ResultColumns) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return handleNoRows(stream) + } + return fmt.Errorf("failed to scan row: %w", err) + } + + err = stream.Send(&querypb.ExecuteQueryResponse{ + Result: &querypb.ExecuteQueryResponse_RowResults{ + RowResults: &querypb.RowResults{ + Rows: result, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to send row results: %w", err) + } + + case querypb.CommandType_COMMAND_TYPE_MANY: + rows, err := db.QueryContext(ctx, req.RawSql, convertParameters(req.Parameters)...) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return handleNoRows(stream) + } + return connect.NewError(connect.CodeInternal, fmt.Errorf("failed to execute query: %w", err)) + } + defer rows.Close() + + rowCount := int32(0) + batchSize := int32(100) // Default batch size + if req.BatchSize != nil { + batchSize = *req.BatchSize + } + + for rows.Next() { + result, err := scanRowToMap(rows, req.ResultColumns) + if err != nil { + return connect.NewError(connect.CodeInternal, fmt.Errorf("failed to scan row: %w", err)) + } + + rowCount++ + hasMore := rowCount < batchSize + if err := stream.Send(&querypb.ExecuteQueryResponse{ + Result: &querypb.ExecuteQueryResponse_RowResults{ + RowResults: &querypb.RowResults{ + Rows: result, + HasMore: hasMore, + }, + }, + }); err != nil { + return fmt.Errorf("failed to send row results: %w", err) + } + + if !hasMore { + rowCount = 0 + } + } + case querypb.CommandType_COMMAND_TYPE_UNSPECIFIED: + return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unknown command type: %v", req.CommandType)) + } + return nil +} + +// scanRowToMap scans a row into a map[string]*querypb.SQLValue where keys are column names and values are the row values +func scanRowToMap(row any, resultColumns []string) (map[string]*querypb.SQLValue, error) { + if len(resultColumns) == 0 { + return nil, fmt.Errorf("result_columns required for scanning rows") + } + + // Get column names from the row + var dbColumns []string + switch r := row.(type) { + case *sql.Rows: + var err error + dbColumns, err = r.Columns() + if err != nil { + return nil, fmt.Errorf("failed to get column names: %w", err) + } + case *sql.Row: + // For sql.Row we can't get column names, but we know they must match our query + dbColumns = resultColumns + default: + return nil, fmt.Errorf("unsupported row type: %T", row) + } + + if len(dbColumns) != len(resultColumns) { + return nil, fmt.Errorf("column count mismatch: got %d columns from DB but expected %d columns", len(dbColumns), len(resultColumns)) + } + + // Create value slices for scanning + values := make([]any, len(dbColumns)) + valuePointers := make([]any, len(dbColumns)) + for i := range values { + valuePointers[i] = &values[i] + } + + var err error + switch r := row.(type) { + case *sql.Row: + err = r.Scan(valuePointers...) + case *sql.Rows: + err = r.Scan(valuePointers...) + } + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + result := make(map[string]*querypb.SQLValue) + for i, val := range values { + col := dbColumns[i] + if val == nil { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_NullValue{NullValue: true}} + continue + } + + switch v := val.(type) { + case []byte: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_BytesValue{BytesValue: v}} + case string: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_StringValue{StringValue: v}} + case int64: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: v}} + case uint64: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: int64(v)}} + case float64: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_FloatValue{FloatValue: v}} + case bool: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_BoolValue{BoolValue: v}} + case time.Time: + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_TimestampValue{TimestampValue: timestamppb.New(v)}} + default: + // Try to convert numeric types + if iv, ok := val.(int); ok { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: int64(iv)}} + } else if iv32, ok := val.(int32); ok { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: int64(iv32)}} + } else if uv, ok := val.(uint); ok { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: int64(uv)}} + } else if uv32, ok := val.(uint32); ok { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_IntValue{IntValue: int64(uv32)}} + } else if fv32, ok := val.(float32); ok { + result[col] = &querypb.SQLValue{Value: &querypb.SQLValue_FloatValue{FloatValue: float64(fv32)}} + } else { + return nil, fmt.Errorf("unsupported value type for column %s: %T with value %v", col, val, val) + } + } + } + + return result, nil +} + +func convertParameters(params []*querypb.SQLValue) []interface{} { + if params == nil { + return nil + } + result := make([]interface{}, len(params)) + for i, p := range params { + if p == nil { + result[i] = nil + continue + } + switch v := p.Value.(type) { + case *querypb.SQLValue_StringValue: + result[i] = v.StringValue + case *querypb.SQLValue_IntValue: + result[i] = v.IntValue + case *querypb.SQLValue_FloatValue: + result[i] = v.FloatValue + case *querypb.SQLValue_BoolValue: + result[i] = v.BoolValue + case *querypb.SQLValue_BytesValue: + result[i] = v.BytesValue + case *querypb.SQLValue_TimestampValue: + result[i] = v.TimestampValue.AsTime() + case *querypb.SQLValue_NullValue: + result[i] = nil + default: + result[i] = nil + } + } + return result +} + +func getDriverName(engine string) string { + switch engine { + case "postgres": + return "postgres" + default: + return "mysql" + } +} + +func handleNoRows(stream *connect.ServerStream[querypb.ExecuteQueryResponse]) error { + err := stream.Send(&querypb.ExecuteQueryResponse{ + Result: &querypb.ExecuteQueryResponse_RowResults{ + RowResults: &querypb.RowResults{ + Rows: make(map[string]*querypb.SQLValue), + HasMore: false, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to send no rows response: %w", err) + } + return nil +} diff --git a/backend/runner/query/service_test.go b/backend/runner/query/service_test.go new file mode 100644 index 0000000000..396f96398a --- /dev/null +++ b/backend/runner/query/service_test.go @@ -0,0 +1,155 @@ +package query + +import ( + "context" + "database/sql" + "net/url" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/alecthomas/assert/v2" + _ "github.com/mattn/go-sqlite3" + + querypb "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1" +) + +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() + db, err := sql.Open("sqlite3", ":memory:") + assert.NoError(t, err) + + _, err = db.Exec(` + CREATE TABLE test_table ( + id INTEGER PRIMARY KEY, + name TEXT, + value INTEGER + ) + `) + assert.NoError(t, err) + + // Insert some test data + _, err = db.Exec(` + INSERT INTO test_table (id, name, value) VALUES + (1, 'test1', 100), + (2, 'test2', 200), + (3, 'test3', 300) + `) + assert.NoError(t, err) + + return db +} + +func TestQueryService(t *testing.T) { + t.Parallel() + db := setupTestDB(t) + t.Cleanup(func() { + db.Close() + }) + + svc := &Service{ + transactions: make(map[string]*sql.Tx), + db: db, + lock: sync.RWMutex{}, + } + + ctx := context.Background() + + t.Run("TransactionLifecycle", func(t *testing.T) { + t.Parallel() + beginResp, err := svc.BeginTransaction(ctx, connect.NewRequest(&querypb.BeginTransactionRequest{})) + assert.NoError(t, err) + assert.NotZero(t, beginResp.Msg.TransactionId) + assert.Equal(t, querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, beginResp.Msg.Status) + + txID := beginResp.Msg.TransactionId + + svc.lock.RLock() + _, exists := svc.transactions[txID] + svc.lock.RUnlock() + assert.True(t, exists) + + commitResp, err := svc.CommitTransaction(ctx, connect.NewRequest(&querypb.CommitTransactionRequest{ + TransactionId: txID, + })) + assert.NoError(t, err) + assert.Equal(t, querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, commitResp.Msg.Status) + + svc.lock.RLock() + _, exists = svc.transactions[txID] + svc.lock.RUnlock() + assert.False(t, exists) + }) + + t.Run("TransactionRollback", func(t *testing.T) { + t.Parallel() + beginResp, err := svc.BeginTransaction(ctx, connect.NewRequest(&querypb.BeginTransactionRequest{})) + assert.NoError(t, err) + txID := beginResp.Msg.TransactionId + + svc.lock.RLock() + _, exists := svc.transactions[txID] + svc.lock.RUnlock() + assert.True(t, exists) + + rollbackResp, err := svc.RollbackTransaction(ctx, connect.NewRequest(&querypb.RollbackTransactionRequest{ + TransactionId: txID, + })) + assert.NoError(t, err) + assert.Equal(t, querypb.TransactionStatus_TRANSACTION_STATUS_SUCCESS, rollbackResp.Msg.Status) + + svc.lock.RLock() + _, exists = svc.transactions[txID] + svc.lock.RUnlock() + assert.False(t, exists) + }) + + t.Run("InvalidTransactionID", func(t *testing.T) { + t.Parallel() + _, err := svc.CommitTransaction(ctx, connect.NewRequest(&querypb.CommitTransactionRequest{ + TransactionId: "invalid", + })) + assert.Error(t, err) + + _, err = svc.RollbackTransaction(ctx, connect.NewRequest(&querypb.RollbackTransactionRequest{ + TransactionId: "invalid", + })) + assert.Error(t, err) + }) +} + +func TestServiceConfig(t *testing.T) { + t.Run("ValidConfig", func(t *testing.T) { + config := &Config{ + Endpoint: &url.URL{Scheme: "http", Host: "localhost:8896"}, + Engine: "mysql", + DSN: "user:pass@tcp(localhost:3306)/db", + } + assert.NoError(t, config.Validate()) + }) + + t.Run("InvalidEngine", func(t *testing.T) { + config := &Config{ + Endpoint: &url.URL{Scheme: "http", Host: "localhost:8896"}, + Engine: "invalid", + DSN: "user:pass@tcp(localhost:3306)/db", + } + assert.Error(t, config.Validate()) + }) + + t.Run("MissingBind", func(t *testing.T) { + config := &Config{ + Engine: "mysql", + DSN: "user:pass@tcp(localhost:3306)/db", + } + assert.Error(t, config.Validate()) + }) + + t.Run("MissingDSN", func(t *testing.T) { + config := &Config{ + Endpoint: &url.URL{Scheme: "http", Host: "localhost:8896"}, + Engine: "mysql", + } + assert.Error(t, config.Validate()) + }) +} diff --git a/backend/runner/runner.go b/backend/runner/runner.go index 0b802fbd2e..5c22cf63ac 100644 --- a/backend/runner/runner.go +++ b/backend/runner/runner.go @@ -33,11 +33,13 @@ import ( ftldeploymentconnect "github.com/block/ftl/backend/protos/xyz/block/ftl/deployment/v1/deploymentpbconnect" ftlleaseconnect "github.com/block/ftl/backend/protos/xyz/block/ftl/lease/v1/leasepbconnect" "github.com/block/ftl/backend/protos/xyz/block/ftl/pubsub/v1/pubsubpbconnect" + "github.com/block/ftl/backend/protos/xyz/block/ftl/query/v1/querypbconnect" ftlv1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1" "github.com/block/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" "github.com/block/ftl/backend/runner/observability" "github.com/block/ftl/backend/runner/proxy" "github.com/block/ftl/backend/runner/pubsub" + "github.com/block/ftl/backend/runner/query" "github.com/block/ftl/common/plugin" "github.com/block/ftl/common/schema" "github.com/block/ftl/common/slices" @@ -60,6 +62,7 @@ type Config struct { Key key.Runner `help:"Runner key (auto)."` ControllerEndpoint *url.URL `name:"ftl-endpoint" help:"Controller endpoint." env:"FTL_ENDPOINT" default:"http://127.0.0.1:8892"` LeaseEndpoint *url.URL `name:"ftl-lease-endpoint" help:"Lease endpoint endpoint." env:"FTL_LEASE_ENDPOINT" default:"http://127.0.0.1:8895"` + QueryEndpoint *url.URL `name:"ftl-query-endpoint" help:"Query endpoint." env:"FTL_QUERY_ENDPOINT" default:"http://127.0.0.1:8897"` TimelineEndpoint *url.URL `help:"Timeline endpoint." env:"FTL_TIMELINE_ENDPOINT" default:"http://127.0.0.1:8894"` TemplateDir string `help:"Template directory to copy into each deployment, if any." type:"existingdir"` DeploymentDir string `help:"Directory to store deployments in." default:"${deploymentdir}"` @@ -126,6 +129,7 @@ func Start(ctx context.Context, config Config, storage *artefacts.OCIArtefactSer cancelFunc: doneFunc, devEndpoint: config.DevEndpoint, devRunnerInfoFile: config.DevRunnerInfoFile, + queryServices: query.NewMultiService(), } module, err := svc.getModule(ctx, config.Deployment) @@ -143,6 +147,9 @@ func Start(ctx context.Context, config Config, storage *artefacts.OCIArtefactSer g.Go(func() error { return svc.startMySQLProxy(ctx, module, startedLatch, dbAddresses) }) + g.Go(func() error { + return svc.startQueryServices(ctx, module) + }) g.Go(func() error { startedLatch.Wait() select { @@ -263,6 +270,7 @@ type Service struct { devRunnerInfoFile optional.Option[string] proxy *proxy.Service pubSub *pubsub.Service + queryServices *query.MultiService proxyBindAddress *url.URL } @@ -345,7 +353,7 @@ func (s *Service) deploy(ctx context.Context, key key.Deployment, module *schema leaseServiceClient := rpc.Dial(ftlleaseconnect.NewLeaseServiceClient, s.config.LeaseEndpoint.String(), log.Error) - s.proxy = proxy.New(deploymentServiceClient, leaseServiceClient, s.timelineClient) + s.proxy = proxy.New(deploymentServiceClient, leaseServiceClient, s.timelineClient, s.queryServices) pubSub, err := pubsub.New(module, key, s, s.timelineClient) if err != nil { @@ -464,6 +472,11 @@ func (s *Service) deploy(ctx context.Context, key key.Deployment, module *schema func (s *Service) Close() error { s.lock.Lock() defer s.lock.Unlock() + + if s.queryServices != nil { + s.queryServices.Close() + } + depl, ok := s.deployment.Load().Get() if !ok { return connect.NewError(connect.CodeNotFound, errors.New("no deployment")) @@ -594,7 +607,6 @@ func (s *Service) streamLogsLoop(ctx context.Context) { Error: optional.Ptr(errorString), }) } - } func (s *Service) getDeploymentLogger(ctx context.Context, deploymentKey key.Deployment) *log.Logger { @@ -673,6 +685,86 @@ func (s *Service) startPgProxy(ctx context.Context, module *schema.Module, start return nil } +// Run query services for each database +func (s *Service) startQueryServices(ctx context.Context, module *schema.Module) error { + logger := log.FromContext(ctx) + + g, ctx := errgroup.WithContext(ctx) + + for _, decl := range module.Decls { + db, ok := decl.(*schema.Database) + if !ok { + continue + } + + logger.Debugf("Initializing query service for %s database: %s.%s", db.Type, module.Name, db.Name) + + var dsn string + switch conn := db.Runtime.Connections.Write.(type) { + case *schema.DSNDatabaseConnector: + dsn = conn.DSN + default: + return fmt.Errorf("unsupported database connector type: %T", db.Runtime.Connections.Write) + } + + baseURL, err := url.Parse("http://127.0.0.1:0") + if err != nil { + return fmt.Errorf("failed to parse base URL: %w", err) + } + + querySvc, err := query.New(ctx, query.Config{ + DSN: dsn, + Engine: db.Type, + Endpoint: baseURL, + }) + if err != nil { + return fmt.Errorf("failed to create query service for database %s: %w", db.Name, err) + } + + server, err := rpc.NewServer(ctx, baseURL, + rpc.GRPC(querypbconnect.NewQueryServiceHandler, querySvc), + ) + if err != nil { + querySvc.Close() + return fmt.Errorf("failed to create server for database %s: %w", db.Name, err) + } + + urls := server.Bind.Subscribe(nil) + dbName := db.Name + g.Go(func() error { + serverGroup, serverCtx := errgroup.WithContext(ctx) + + serverGroup.Go(func() error { + if err := server.Serve(serverCtx); err != nil && !errors.Is(err, context.Canceled) { + return fmt.Errorf("query service server for database %s stopped unexpectedly: %w", dbName, err) + } + return nil + }) + + select { + case <-serverCtx.Done(): + return serverCtx.Err() + case endpoint := <-urls: + client := rpc.Dial(querypbconnect.NewQueryServiceClient, endpoint.String(), log.Error) + if err := rpc.Wait(serverCtx, backoff.Backoff{}, time.Second*5, client); err != nil { + return fmt.Errorf("query service for database %s failed health check: %w", dbName, err) + } + querySvc.SetEndpoint(endpoint) + if err := s.queryServices.AddService(module.Name, dbName, querySvc); err != nil { + return fmt.Errorf("failed to add query service for database %s: %w", dbName, err) + } + return serverGroup.Wait() + } + }) + } + + err := g.Wait() + if err != nil { + return fmt.Errorf("failed to start query services: %w", err) + } + return nil +} + func (s *Service) startMySQLProxy(ctx context.Context, module *schema.Module, latch *sync.WaitGroup, addresses *xsync.MapOf[string, string]) error { defer latch.Done() logger := log.FromContext(ctx) diff --git a/backend/timeline/testdata/go/echo/go.mod b/backend/timeline/testdata/go/echo/go.mod index 22c33bef34..5c5de2218b 100644 --- a/backend/timeline/testdata/go/echo/go.mod +++ b/backend/timeline/testdata/go/echo/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/backend/timeline/testdata/go/echo/go.sum b/backend/timeline/testdata/go/echo/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/backend/timeline/testdata/go/echo/go.sum +++ b/backend/timeline/testdata/go/echo/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/backend/timeline/testdata/go/publisher/go.mod b/backend/timeline/testdata/go/publisher/go.mod index 4b85aa53ee..b7adfa7377 100644 --- a/backend/timeline/testdata/go/publisher/go.mod +++ b/backend/timeline/testdata/go/publisher/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/backend/timeline/testdata/go/publisher/go.sum b/backend/timeline/testdata/go/publisher/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/backend/timeline/testdata/go/publisher/go.sum +++ b/backend/timeline/testdata/go/publisher/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/backend/timeline/testdata/go/time/go.mod b/backend/timeline/testdata/go/time/go.mod index 02edaa3b57..63e4adc1f5 100644 --- a/backend/timeline/testdata/go/time/go.mod +++ b/backend/timeline/testdata/go/time/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/backend/timeline/testdata/go/time/go.sum b/backend/timeline/testdata/go/time/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/backend/timeline/testdata/go/time/go.sum +++ b/backend/timeline/testdata/go/time/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/common/encoding/encoding.go b/common/encoding/encoding.go index 186909ea3d..30c47d6995 100644 --- a/common/encoding/encoding.go +++ b/common/encoding/encoding.go @@ -306,28 +306,36 @@ func decodeStruct(d *json.Decoder, v reflect.Value) error { } for d.More() { - token, err := d.Token() + keyToken, err := d.Token() if err != nil { return err } - key, ok := token.(string) + key, ok := keyToken.(string) if !ok { - return fmt.Errorf("expected string key, got %T", token) + return fmt.Errorf("expected string key, got %T (value: %v)", keyToken, keyToken) } field := v.FieldByNameFunc(func(s string) bool { return strcase.ToLowerCamel(s) == key }) if !field.IsValid() { - // Issue #2117 #2119: ignore unknown fields + // Skip the value token for unknown fields + if _, err := d.Token(); err != nil { + return fmt.Errorf("failed to skip unknown field %s: %w", key, err) + } continue } + fieldTypeStr := field.Type().String() switch { case fieldTypeStr == "*Unit" || fieldTypeStr == "Unit": if fieldTypeStr == "*Unit" && field.IsNil() { field.Set(reflect.New(field.Type().Elem())) } + // Skip the value token for Unit types + if _, err := d.Token(); err != nil { + return fmt.Errorf("failed to skip Unit field %s: %w", key, err) + } default: if err := decodeValue(d, field); err != nil { return err diff --git a/common/reflection/query.go b/common/reflection/query.go new file mode 100644 index 0000000000..a18cecc5a9 --- /dev/null +++ b/common/reflection/query.go @@ -0,0 +1,32 @@ +package reflection + +import ( + "reflect" +) + +type CommandType int + +const ( + CommandTypeExec CommandType = iota + CommandTypeOne + CommandTypeMany +) + +func Query( + module string, + verbName string, + queryFunc any, +) Registree { + ref := Ref{ + Module: module, + Name: verbName, + } + return func(t *TypeRegistry) { + vi := verbCall{ + ref: ref, + args: []reflect.Value{}, + fn: reflect.ValueOf(queryFunc), + } + t.verbCalls[ref] = vi + } +} diff --git a/common/schema/verb.go b/common/schema/verb.go index e517de6952..5c5f9689c1 100644 --- a/common/schema/verb.go +++ b/common/schema/verb.go @@ -181,15 +181,15 @@ func (v *Verb) ResourceID() string { // IsGenerated returns true if the Verb is in the schema but not in the source code. func (v *Verb) IsGenerated() bool { - _, ok := v.GetRawSQLQuery() + _, ok := v.GetQuery() return ok } -// GetRawSQLQuery returns the raw SQL query for the Verb if it exists. If present, the Verb was generated from SQL. -func (v *Verb) GetRawSQLQuery() (string, bool) { +// GetQuery returns the query metadata for the Verb if it exists. If present, the Verb was generated from SQL. +func (v *Verb) GetQuery() (*MetadataSQLQuery, bool) { md, found := slices.FindVariant[*MetadataSQLQuery](v.Metadata) if !found { - return "", false + return nil, false } - return md.Query, true + return md, true } diff --git a/examples/go/echo/go.mod b/examples/go/echo/go.mod index 6b863a5c6d..0419fbe8b2 100644 --- a/examples/go/echo/go.mod +++ b/examples/go/echo/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/examples/go/echo/go.sum b/examples/go/echo/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/examples/go/echo/go.sum +++ b/examples/go/echo/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/examples/go/mysql/db/schema/testdb/20241103205515_init.sql b/examples/go/mysql/db/schema/testdb/20241103205515_init.sql new file mode 100644 index 0000000000..7c43e01442 --- /dev/null +++ b/examples/go/mysql/db/schema/testdb/20241103205515_init.sql @@ -0,0 +1,10 @@ +-- migrate:up +CREATE TABLE authors ( + id SERIAL PRIMARY KEY, + bio text NULL DEFAULT NULL, + birth_year int NULL DEFAULT NULL, + hometown text NULL DEFAULT NULL +); + +-- migrate:down +DROP TABLE authors; diff --git a/examples/go/mysql/db/schema/testdb/20241103205516_init.sql b/examples/go/mysql/db/schema/testdb/20241103205516_init.sql new file mode 100644 index 0000000000..2bbe10af9d --- /dev/null +++ b/examples/go/mysql/db/schema/testdb/20241103205516_init.sql @@ -0,0 +1,5 @@ +-- migrate:up +CREATE TABLE foo (a text, b integer, c DATETIME, d DATE); + +-- migrate:down +DROP TABLE foo; diff --git a/examples/go/mysql/db/schema/testdb/20241103205517_init.sql b/examples/go/mysql/db/schema/testdb/20241103205517_init.sql new file mode 100644 index 0000000000..4f58b9d870 --- /dev/null +++ b/examples/go/mysql/db/schema/testdb/20241103205517_init.sql @@ -0,0 +1,7 @@ +-- migrate:up +CREATE TABLE records ( + id SERIAL PRIMARY KEY +); + +-- migrate:down +DROP TABLE records; diff --git a/examples/go/pubsub/go.mod b/examples/go/pubsub/go.mod index 3dc7fd90c0..9d40ccbc5b 100644 --- a/examples/go/pubsub/go.mod +++ b/examples/go/pubsub/go.mod @@ -39,6 +39,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/examples/go/pubsub/go.sum b/examples/go/pubsub/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/examples/go/pubsub/go.sum +++ b/examples/go/pubsub/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/examples/go/time/go.mod b/examples/go/time/go.mod index b99e6748fb..2edba40b05 100644 --- a/examples/go/time/go.mod +++ b/examples/go/time/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/examples/go/time/go.sum b/examples/go/time/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/examples/go/time/go.sum +++ b/examples/go/time/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/frontend/cli/testdata/go/echo/go.mod b/frontend/cli/testdata/go/echo/go.mod index 65867a4f3d..09be1f2f75 100644 --- a/frontend/cli/testdata/go/echo/go.mod +++ b/frontend/cli/testdata/go/echo/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/frontend/cli/testdata/go/echo/go.sum b/frontend/cli/testdata/go/echo/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/frontend/cli/testdata/go/echo/go.sum +++ b/frontend/cli/testdata/go/echo/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/frontend/cli/testdata/go/time/go.mod b/frontend/cli/testdata/go/time/go.mod index 02edaa3b57..63e4adc1f5 100644 --- a/frontend/cli/testdata/go/time/go.mod +++ b/frontend/cli/testdata/go/time/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/frontend/cli/testdata/go/time/go.sum b/frontend/cli/testdata/go/time/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/frontend/cli/testdata/go/time/go.sum +++ b/frontend/cli/testdata/go/time/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/frontend/console/src/protos/xyz/block/ftl/query/v1/query_connect.ts b/frontend/console/src/protos/xyz/block/ftl/query/v1/query_connect.ts new file mode 100644 index 0000000000..6948a8c2dc --- /dev/null +++ b/frontend/console/src/protos/xyz/block/ftl/query/v1/query_connect.ts @@ -0,0 +1,74 @@ +// @generated by protoc-gen-connect-es v1.6.1 with parameter "target=ts" +// @generated from file xyz/block/ftl/query/v1/query.proto (package xyz.block.ftl.query.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { PingRequest, PingResponse } from "../../v1/ftl_pb.js"; +import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; +import { BeginTransactionRequest, BeginTransactionResponse, CommitTransactionRequest, CommitTransactionResponse, ExecuteQueryRequest, ExecuteQueryResponse, RollbackTransactionRequest, RollbackTransactionResponse } from "./query_pb.js"; + +/** + * @generated from service xyz.block.ftl.query.v1.QueryService + */ +export const QueryService = { + typeName: "xyz.block.ftl.query.v1.QueryService", + methods: { + /** + * Ping service for readiness + * + * @generated from rpc xyz.block.ftl.query.v1.QueryService.Ping + */ + ping: { + name: "Ping", + I: PingRequest, + O: PingResponse, + kind: MethodKind.Unary, + idempotency: MethodIdempotency.NoSideEffects, + }, + /** + * Begins a new transaction and returns a transaction ID. + * + * @generated from rpc xyz.block.ftl.query.v1.QueryService.BeginTransaction + */ + beginTransaction: { + name: "BeginTransaction", + I: BeginTransactionRequest, + O: BeginTransactionResponse, + kind: MethodKind.Unary, + }, + /** + * Commits a transaction. + * + * @generated from rpc xyz.block.ftl.query.v1.QueryService.CommitTransaction + */ + commitTransaction: { + name: "CommitTransaction", + I: CommitTransactionRequest, + O: CommitTransactionResponse, + kind: MethodKind.Unary, + }, + /** + * Rolls back a transaction. + * + * @generated from rpc xyz.block.ftl.query.v1.QueryService.RollbackTransaction + */ + rollbackTransaction: { + name: "RollbackTransaction", + I: RollbackTransactionRequest, + O: RollbackTransactionResponse, + kind: MethodKind.Unary, + }, + /** + * Executes a raw SQL query, optionally within a transaction. + * + * @generated from rpc xyz.block.ftl.query.v1.QueryService.ExecuteQuery + */ + executeQuery: { + name: "ExecuteQuery", + I: ExecuteQueryRequest, + O: ExecuteQueryResponse, + kind: MethodKind.ServerStreaming, + }, + } +} as const; + diff --git a/frontend/console/src/protos/xyz/block/ftl/query/v1/query_pb.ts b/frontend/console/src/protos/xyz/block/ftl/query/v1/query_pb.ts new file mode 100644 index 0000000000..3e2d0842e5 --- /dev/null +++ b/frontend/console/src/protos/xyz/block/ftl/query/v1/query_pb.ts @@ -0,0 +1,596 @@ +// @generated by protoc-gen-es v1.10.0 with parameter "target=ts" +// @generated from file xyz/block/ftl/query/v1/query.proto (package xyz.block.ftl.query.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import { Message, proto3, protoInt64, Timestamp } from "@bufbuild/protobuf"; + +/** + * @generated from enum xyz.block.ftl.query.v1.TransactionStatus + */ +export enum TransactionStatus { + /** + * @generated from enum value: TRANSACTION_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: TRANSACTION_STATUS_SUCCESS = 1; + */ + SUCCESS = 1, + + /** + * @generated from enum value: TRANSACTION_STATUS_FAILED = 2; + */ + FAILED = 2, +} +// Retrieve enum metadata with: proto3.getEnumType(TransactionStatus) +proto3.util.setEnumType(TransactionStatus, "xyz.block.ftl.query.v1.TransactionStatus", [ + { no: 0, name: "TRANSACTION_STATUS_UNSPECIFIED" }, + { no: 1, name: "TRANSACTION_STATUS_SUCCESS" }, + { no: 2, name: "TRANSACTION_STATUS_FAILED" }, +]); + +/** + * @generated from enum xyz.block.ftl.query.v1.CommandType + */ +export enum CommandType { + /** + * @generated from enum value: COMMAND_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: COMMAND_TYPE_EXEC = 1; + */ + EXEC = 1, + + /** + * @generated from enum value: COMMAND_TYPE_ONE = 2; + */ + ONE = 2, + + /** + * @generated from enum value: COMMAND_TYPE_MANY = 3; + */ + MANY = 3, +} +// Retrieve enum metadata with: proto3.getEnumType(CommandType) +proto3.util.setEnumType(CommandType, "xyz.block.ftl.query.v1.CommandType", [ + { no: 0, name: "COMMAND_TYPE_UNSPECIFIED" }, + { no: 1, name: "COMMAND_TYPE_EXEC" }, + { no: 2, name: "COMMAND_TYPE_ONE" }, + { no: 3, name: "COMMAND_TYPE_MANY" }, +]); + +/** + * @generated from message xyz.block.ftl.query.v1.BeginTransactionRequest + */ +export class BeginTransactionRequest extends Message { + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.BeginTransactionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): BeginTransactionRequest { + return new BeginTransactionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): BeginTransactionRequest { + return new BeginTransactionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): BeginTransactionRequest { + return new BeginTransactionRequest().fromJsonString(jsonString, options); + } + + static equals(a: BeginTransactionRequest | PlainMessage | undefined, b: BeginTransactionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(BeginTransactionRequest, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.BeginTransactionResponse + */ +export class BeginTransactionResponse extends Message { + /** + * @generated from field: string transaction_id = 1; + */ + transactionId = ""; + + /** + * @generated from field: xyz.block.ftl.query.v1.TransactionStatus status = 2; + */ + status = TransactionStatus.UNSPECIFIED; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.BeginTransactionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "transaction_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "status", kind: "enum", T: proto3.getEnumType(TransactionStatus) }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): BeginTransactionResponse { + return new BeginTransactionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): BeginTransactionResponse { + return new BeginTransactionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): BeginTransactionResponse { + return new BeginTransactionResponse().fromJsonString(jsonString, options); + } + + static equals(a: BeginTransactionResponse | PlainMessage | undefined, b: BeginTransactionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(BeginTransactionResponse, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.CommitTransactionRequest + */ +export class CommitTransactionRequest extends Message { + /** + * @generated from field: string transaction_id = 1; + */ + transactionId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.CommitTransactionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "transaction_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CommitTransactionRequest { + return new CommitTransactionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CommitTransactionRequest { + return new CommitTransactionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CommitTransactionRequest { + return new CommitTransactionRequest().fromJsonString(jsonString, options); + } + + static equals(a: CommitTransactionRequest | PlainMessage | undefined, b: CommitTransactionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(CommitTransactionRequest, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.CommitTransactionResponse + */ +export class CommitTransactionResponse extends Message { + /** + * @generated from field: xyz.block.ftl.query.v1.TransactionStatus status = 1; + */ + status = TransactionStatus.UNSPECIFIED; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.CommitTransactionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "status", kind: "enum", T: proto3.getEnumType(TransactionStatus) }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CommitTransactionResponse { + return new CommitTransactionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CommitTransactionResponse { + return new CommitTransactionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CommitTransactionResponse { + return new CommitTransactionResponse().fromJsonString(jsonString, options); + } + + static equals(a: CommitTransactionResponse | PlainMessage | undefined, b: CommitTransactionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(CommitTransactionResponse, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.RollbackTransactionRequest + */ +export class RollbackTransactionRequest extends Message { + /** + * @generated from field: string transaction_id = 1; + */ + transactionId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.RollbackTransactionRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "transaction_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RollbackTransactionRequest { + return new RollbackTransactionRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RollbackTransactionRequest { + return new RollbackTransactionRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RollbackTransactionRequest { + return new RollbackTransactionRequest().fromJsonString(jsonString, options); + } + + static equals(a: RollbackTransactionRequest | PlainMessage | undefined, b: RollbackTransactionRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(RollbackTransactionRequest, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.RollbackTransactionResponse + */ +export class RollbackTransactionResponse extends Message { + /** + * @generated from field: xyz.block.ftl.query.v1.TransactionStatus status = 1; + */ + status = TransactionStatus.UNSPECIFIED; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.RollbackTransactionResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "status", kind: "enum", T: proto3.getEnumType(TransactionStatus) }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RollbackTransactionResponse { + return new RollbackTransactionResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RollbackTransactionResponse { + return new RollbackTransactionResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RollbackTransactionResponse { + return new RollbackTransactionResponse().fromJsonString(jsonString, options); + } + + static equals(a: RollbackTransactionResponse | PlainMessage | undefined, b: RollbackTransactionResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(RollbackTransactionResponse, a, b); + } +} + +/** + * A value that can be used as a SQL parameter + * + * @generated from message xyz.block.ftl.query.v1.SQLValue + */ +export class SQLValue extends Message { + /** + * @generated from oneof xyz.block.ftl.query.v1.SQLValue.value + */ + value: { + /** + * @generated from field: string string_value = 1; + */ + value: string; + case: "stringValue"; + } | { + /** + * @generated from field: int64 int_value = 2; + */ + value: bigint; + case: "intValue"; + } | { + /** + * @generated from field: double float_value = 3; + */ + value: number; + case: "floatValue"; + } | { + /** + * @generated from field: bool bool_value = 4; + */ + value: boolean; + case: "boolValue"; + } | { + /** + * @generated from field: bytes bytes_value = 5; + */ + value: Uint8Array; + case: "bytesValue"; + } | { + /** + * @generated from field: google.protobuf.Timestamp timestamp_value = 6; + */ + value: Timestamp; + case: "timestampValue"; + } | { + /** + * Set to true to represent NULL + * + * @generated from field: bool null_value = 7; + */ + value: boolean; + case: "nullValue"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.SQLValue"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "string_value", kind: "scalar", T: 9 /* ScalarType.STRING */, oneof: "value" }, + { no: 2, name: "int_value", kind: "scalar", T: 3 /* ScalarType.INT64 */, oneof: "value" }, + { no: 3, name: "float_value", kind: "scalar", T: 1 /* ScalarType.DOUBLE */, oneof: "value" }, + { no: 4, name: "bool_value", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "value" }, + { no: 5, name: "bytes_value", kind: "scalar", T: 12 /* ScalarType.BYTES */, oneof: "value" }, + { no: 6, name: "timestamp_value", kind: "message", T: Timestamp, oneof: "value" }, + { no: 7, name: "null_value", kind: "scalar", T: 8 /* ScalarType.BOOL */, oneof: "value" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SQLValue { + return new SQLValue().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SQLValue { + return new SQLValue().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SQLValue { + return new SQLValue().fromJsonString(jsonString, options); + } + + static equals(a: SQLValue | PlainMessage | undefined, b: SQLValue | PlainMessage | undefined): boolean { + return proto3.util.equals(SQLValue, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.ExecuteQueryRequest + */ +export class ExecuteQueryRequest extends Message { + /** + * @generated from field: string raw_sql = 1; + */ + rawSql = ""; + + /** + * @generated from field: xyz.block.ftl.query.v1.CommandType command_type = 2; + */ + commandType = CommandType.UNSPECIFIED; + + /** + * SQL parameter values in order + * + * @generated from field: repeated xyz.block.ftl.query.v1.SQLValue parameters = 3; + */ + parameters: SQLValue[] = []; + + /** + * Column names to scan for the result type + * + * @generated from field: repeated string result_columns = 6; + */ + resultColumns: string[] = []; + + /** + * @generated from field: optional string transaction_id = 4; + */ + transactionId?: string; + + /** + * Default 100 if not set + * + * @generated from field: optional int32 batch_size = 5; + */ + batchSize?: number; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.ExecuteQueryRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "raw_sql", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "command_type", kind: "enum", T: proto3.getEnumType(CommandType) }, + { no: 3, name: "parameters", kind: "message", T: SQLValue, repeated: true }, + { no: 6, name: "result_columns", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 4, name: "transaction_id", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 5, name: "batch_size", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ExecuteQueryRequest { + return new ExecuteQueryRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ExecuteQueryRequest { + return new ExecuteQueryRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ExecuteQueryRequest { + return new ExecuteQueryRequest().fromJsonString(jsonString, options); + } + + static equals(a: ExecuteQueryRequest | PlainMessage | undefined, b: ExecuteQueryRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(ExecuteQueryRequest, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.ExecuteQueryResponse + */ +export class ExecuteQueryResponse extends Message { + /** + * @generated from oneof xyz.block.ftl.query.v1.ExecuteQueryResponse.result + */ + result: { + /** + * For EXEC commands + * + * @generated from field: xyz.block.ftl.query.v1.ExecResult exec_result = 1; + */ + value: ExecResult; + case: "execResult"; + } | { + /** + * For ONE/MANY commands + * + * @generated from field: xyz.block.ftl.query.v1.RowResults row_results = 2; + */ + value: RowResults; + case: "rowResults"; + } | { case: undefined; value?: undefined } = { case: undefined }; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.ExecuteQueryResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "exec_result", kind: "message", T: ExecResult, oneof: "result" }, + { no: 2, name: "row_results", kind: "message", T: RowResults, oneof: "result" }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ExecuteQueryResponse { + return new ExecuteQueryResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ExecuteQueryResponse { + return new ExecuteQueryResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ExecuteQueryResponse { + return new ExecuteQueryResponse().fromJsonString(jsonString, options); + } + + static equals(a: ExecuteQueryResponse | PlainMessage | undefined, b: ExecuteQueryResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(ExecuteQueryResponse, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.ExecResult + */ +export class ExecResult extends Message { + /** + * @generated from field: int64 rows_affected = 1; + */ + rowsAffected = protoInt64.zero; + + /** + * Only for some databases like MySQL + * + * @generated from field: optional int64 last_insert_id = 2; + */ + lastInsertId?: bigint; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.ExecResult"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "rows_affected", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, + { no: 2, name: "last_insert_id", kind: "scalar", T: 3 /* ScalarType.INT64 */, opt: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ExecResult { + return new ExecResult().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ExecResult { + return new ExecResult().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ExecResult { + return new ExecResult().fromJsonString(jsonString, options); + } + + static equals(a: ExecResult | PlainMessage | undefined, b: ExecResult | PlainMessage | undefined): boolean { + return proto3.util.equals(ExecResult, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.query.v1.RowResults + */ +export class RowResults extends Message { + /** + * Each row is a map of column name to value + * + * @generated from field: map rows = 1; + */ + rows: { [key: string]: SQLValue } = {}; + + /** + * Indicates if there are more rows to fetch + * + * @generated from field: bool has_more = 2; + */ + hasMore = false; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.query.v1.RowResults"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "rows", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: SQLValue} }, + { no: 2, name: "has_more", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RowResults { + return new RowResults().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RowResults { + return new RowResults().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RowResults { + return new RowResults().fromJsonString(jsonString, options); + } + + static equals(a: RowResults | PlainMessage | undefined, b: RowResults | PlainMessage | undefined): boolean { + return proto3.util.equals(RowResults, a, b); + } +} + diff --git a/go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl b/go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl index edcd721467..2dc23eaaf7 100644 --- a/go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl +++ b/go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl @@ -33,6 +33,7 @@ func init() { {{- end }} {{- end}} {{- range $verbs}} + {{- if not .IsQuery }} reflection.ProvideResourcesForVerb( {{.TypeName}}, {{- range .Resources}} @@ -61,6 +62,7 @@ func init() { {{- end }} {{- end}} ), + {{- end }} {{- end}} ) } @@ -70,13 +72,13 @@ func main() { verbConstructor := server.NewUserVerbServer("{{.ProjectName}}", "{{$name}}", {{- range $verbs }} {{- if and (eq .Request.TypeName "ftl.Unit") (eq .Response.TypeName "ftl.Unit") }} - server.HandleEmpty({{.TypeName}}), + server.HandleEmpty("{{$name}}", "{{.Name}}"), {{- else if eq .Request.TypeName "ftl.Unit" }} - server.HandleSource[{{.Response.TypeName}}]({{.TypeName}}), + server.HandleSource[{{.Response.TypeName}}]("{{$name}}", "{{.Name}}"), {{- else if eq .Response.TypeName "ftl.Unit" }} - server.HandleSink[{{.Request.TypeName}}]({{.TypeName}}), + server.HandleSink[{{.Request.TypeName}}]("{{$name}}", "{{.Name}}"), {{- else }} - server.HandleCall[{{.Request.TypeName}}, {{.Response.TypeName}}]({{.TypeName}}), + server.HandleCall[{{.Request.TypeName}}, {{.Response.TypeName}}]("{{$name}}", "{{.Name}}"), {{- end -}} {{- end}} ) diff --git a/go-runtime/compile/build-template/types.ftl.go.tmpl b/go-runtime/compile/build-template/types.ftl.go.tmpl index 4902abf3f8..a7cdd29460 100644 --- a/go-runtime/compile/build-template/types.ftl.go.tmpl +++ b/go-runtime/compile/build-template/types.ftl.go.tmpl @@ -16,6 +16,7 @@ import ( {{ end }} {{- range $verbs -}} + {{ if not .IsQuery }} {{ $req := .Request.LocalTypeName -}} {{ $resp := .Response.LocalTypeName -}} @@ -28,6 +29,7 @@ type {{.Name|title}}Client func(context.Context, {{$req}}) error {{- else }} type {{.Name|title}}Client func(context.Context, {{$req}}) ({{$resp}}, error) {{- end }} + {{ end }} {{ end -}} {{- if or .SumTypes .ExternalTypes $verbs }} @@ -51,6 +53,7 @@ func init() { {{- end }} {{- end}} {{- range $verbs}} + {{ if not .IsQuery }} reflection.ProvideResourcesForVerb( {{ trimModuleQualifier $moduleName .TypeName }}, {{- range .Resources}} @@ -81,6 +84,7 @@ func init() { {{- end }} {{- end}} ), + {{- end }} {{- end}} ) } diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index 388897f440..559a9acb55 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -28,6 +28,7 @@ import ( "github.com/block/ftl/common/reflect" "github.com/block/ftl/common/schema" islices "github.com/block/ftl/common/slices" + "github.com/block/ftl/common/strcase" extract "github.com/block/ftl/go-runtime/schema" "github.com/block/ftl/go-runtime/schema/common" "github.com/block/ftl/go-runtime/schema/finalize" @@ -98,12 +99,19 @@ func (c *mainDeploymentContext) generateMainImports() []string { func (c *mainDeploymentContext) generateQueryImports() []string { imports := sets.NewSet[string]() imports.Add(`"context"`) - for _, d := range c.QueriesCtx.Decls { - if data, ok := d.(*schema.Data); ok { - for _, f := range data.Fields { - if _, ok := f.Type.(*schema.Time); ok { - imports.Add(`stdtime "time"`) - } + imports.Add(`"github.com/block/ftl/go-runtime/server"`) + imports.Add(`"github.com/block/ftl/common/reflection"`) + imports.Add(`"github.com/alecthomas/types/tuple"`) + for _, d := range c.QueriesCtx.Data { + for _, f := range d.Fields { + if usesType(f.Type, &schema.Time{}) { + imports.Add(`stdtime "time"`) + } + if usesType(f.Type, &schema.Unit{}) { + imports.Add(`"github.com/block/ftl/go-runtime/ftl"`) + } + if usesType(f.Type, &schema.Optional{}) { + imports.Add(`"github.com/block/ftl/go-runtime/ftl"`) } } } @@ -209,8 +217,20 @@ type typesFileContext struct { type queriesFileContext struct { Module *schema.Module - Decls []schema.Decl + Verbs []queryVerb + Data []*schema.Data Imports []string + + // temporarily using a global DB name/assuming 1 DB per module + DBName string +} + +type queryVerb struct { + *schema.Verb + CommandType string + RawSQL string + ParamFields string + ColToFieldName string } type goType interface { @@ -237,9 +257,11 @@ func (n nativeType) TypeName() string { } type goVerb struct { + Name string Request goSchemaType Response goSchemaType Resources []verbResource + IsQuery bool nativeType } @@ -596,7 +618,7 @@ func scaffoldBuildTemplateAndTidy(ctx context.Context, config moduleconfig.AbsMo scaffolder.Functions(funcs)); err != nil { return fmt.Errorf("failed to scaffold build template: %w", err) } - if len(mctx.QueriesCtx.Decls) > 0 { + if len(mctx.QueriesCtx.Verbs) > 0 { if err := internal.ScaffoldZip(queriesTemplateFiles(), config.Dir, mctx, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { return fmt.Errorf("failed to scaffold queries template: %w", err) @@ -653,6 +675,7 @@ type mainDeploymentContextBuilder struct { topicMapperNames map[*schema.Topic]finalize.TopicMapperQualifiedNames verbResourceParamOrders map[*schema.Verb][]common.VerbResourceParam imports map[string]string + visited sets.Set[string] } func buildMainDeploymentContext(sch *schema.Schema, result extract.Result, goModVersion, projectName string, @@ -671,6 +694,7 @@ func buildMainDeploymentContext(sch *schema.Schema, result extract.Result, goMod topicMapperNames: result.TopicPartitionMapperNames, verbResourceParamOrders: result.VerbResourceParamOrder, imports: imports(result.Module, false), + visited: sets.NewSet[string](), } return builder.build(goModVersion, ftlVersion, projectName, sharedModulesPaths, replacements) } @@ -696,12 +720,12 @@ func (b *mainDeploymentContextBuilder) build(goModVersion, ftlVersion, projectNa }, QueriesCtx: queriesFileContext{ Module: b.mainModule, - Decls: []schema.Decl{}, + Verbs: []queryVerb{}, + Data: []*schema.Data{}, }, } - visited := sets.NewSet[string]() - err := b.visit(ctx, b.mainModule, b.mainModule, visited) + err := b.visit(ctx, b.mainModule, b.mainModule) if err != nil { return mainDeploymentContext{}, err } @@ -719,6 +743,18 @@ func (b *mainDeploymentContextBuilder) build(goModVersion, ftlVersion, projectNa mainModuleImport = fmt.Sprintf("%s %q", alias, mainModuleImport) ctx.TypesCtx.MainModulePkg = alias } + + slices.SortFunc(ctx.QueriesCtx.Verbs, func(a, b queryVerb) int { + return strings.Compare(a.Verb.Name, b.Verb.Name) + }) + slices.SortFunc(ctx.QueriesCtx.Data, func(a, b *schema.Data) int { + return strings.Compare(a.Name, b.Name) + }) + // temporarily assuming there is only one database per module + if len(ctx.Databases) > 0 { + ctx.QueriesCtx.DBName = ctx.Databases[0].Name + } + ctx.withImports(mainModuleImport) return *ctx, nil } @@ -741,19 +777,28 @@ func (b *mainDeploymentContextBuilder) visit( ctx *mainDeploymentContext, module *schema.Module, node schema.Node, - visited sets.Set[string], ) error { err := schema.Visit(node, func(node schema.Node, next func() error) error { switch n := node.(type) { case *schema.Verb: - if _, isQuery := n.GetRawSQLQuery(); isQuery { - decls, err := b.getQueryDecls(n) + if _, isQuery := n.GetQuery(); isQuery { + refName := module.Name + "." + n.Name + if b.visited.Contains(refName) { + return next() + } + b.visited.Add(refName) + verbs, data, err := b.getQueryDecls(n) + if err != nil { + return err + } + ctx.QueriesCtx.Verbs = append(ctx.QueriesCtx.Verbs, verbs...) + ctx.QueriesCtx.Data = append(ctx.QueriesCtx.Data, data...) + verb, err := b.getGoVerb(fmt.Sprintf("ftl/%s.%s", b.mainModule.Name, n.Name), n) if err != nil { return err } - m := &schema.Module{Decls: decls} - schema.SortModuleDecls(m) - ctx.QueriesCtx.Decls = append(ctx.QueriesCtx.Decls, m.Decls...) + verb.IsQuery = true + ctx.Verbs = append(ctx.Verbs, verb) } case *schema.Ref: maybeResolved, maybeModule := b.sch.ResolveWithModule(n) @@ -765,7 +810,7 @@ func (b *mainDeploymentContextBuilder) visit( if !ok { return next() } - err := b.visit(ctx, m, resolved, visited) + err := b.visit(ctx, m, resolved) if err != nil { return fmt.Errorf("failed to visit children of %s: %w", n, err) } @@ -780,10 +825,10 @@ func (b *mainDeploymentContextBuilder) visit( if !ok { return next() } - if visited.Contains(gotype.getNativeType().TypeName()) { + if b.visited.Contains(gotype.getNativeType().TypeName()) { return next() } - visited.Add(gotype.getNativeType().TypeName()) + b.visited.Add(gotype.getNativeType().TypeName()) switch n := gotype.(type) { case goVerb: @@ -807,32 +852,88 @@ func (b *mainDeploymentContextBuilder) visit( return nil } -func (b *mainDeploymentContextBuilder) getQueryDecls(node schema.Node) ([]schema.Decl, error) { - var decls []schema.Decl +func (b *mainDeploymentContextBuilder) getQueryDecls(node schema.Node) ([]queryVerb, []*schema.Data, error) { + var verbs []queryVerb + var data []*schema.Data err := schema.Visit(node, func(node schema.Node, next func() error) error { switch n := node.(type) { case *schema.Verb: - decls = append(decls, n) + verbs = append(verbs, b.toQueryVerb(n)) case *schema.Data: - decls = append(decls, n) + refName := b.mainModule.Name + "." + n.Name + if b.visited.Contains(refName) { + return next() + } + b.visited.Add(refName) + data = append(data, n) case *schema.Ref: maybeResolved, _ := b.sch.ResolveWithModule(n) resolved, ok := maybeResolved.Get() if !ok { return next() } - nested, err := b.getQueryDecls(resolved) + nestedVerbs, nestedData, err := b.getQueryDecls(resolved) if err != nil { return err } - decls = append(decls, nested...) + verbs = append(verbs, nestedVerbs...) + data = append(data, nestedData...) } return next() }) if err != nil { - return nil, fmt.Errorf("failed to get query decls: %w", err) + return nil, nil, fmt.Errorf("failed to get query decls: %w", err) + } + return verbs, data, nil +} + +func (b *mainDeploymentContextBuilder) toQueryVerb(verb *schema.Verb) queryVerb { + request := b.getQueryRequestResponseData(verb.Request) + response := b.getQueryRequestResponseData(verb.Response) + + var params []string + if request != nil { + for _, field := range request.Fields { + if _, ok := islices.FindVariant[*schema.MetadataDBColumn](field.Metadata); ok { + // casing field name with the same mechanism as the generated code + params = append(params, strings.Title(field.Name)) + } + } + } + + var pairs []string + if response != nil { + for _, field := range response.Fields { + if md, ok := islices.FindVariant[*schema.MetadataDBColumn](field.Metadata); ok { + // casing field name with the same mechanism as the generated code + pairs = append(pairs, fmt.Sprintf("tuple.PairOf(%q, %q)", md.Name, strings.Title(field.Name))) + } + } + } + + sqlQuery, _ := verb.GetQuery() + return queryVerb{ + Verb: verb, + CommandType: strcase.ToUpperCamel(strings.TrimPrefix(sqlQuery.Command, ":")), + RawSQL: sqlQuery.Query, + ParamFields: fmt.Sprintf("[]string{%s}", strings.Join(islices.Map(params, strconv.Quote), ",")), + ColToFieldName: fmt.Sprintf("[]tuple.Pair[string,string]{%s}", strings.Join(pairs, ",")), + } +} + +func (b *mainDeploymentContextBuilder) getQueryRequestResponseData(reqResp schema.Type) *schema.Data { + switch r := reqResp.(type) { + case *schema.Ref: + resolved, ok := b.sch.Resolve(r).Get() + if !ok { + return nil + } + return resolved.(*schema.Data) //nolint:forcetypeassert + case *schema.Array: + return b.getQueryRequestResponseData(r.Element) + default: + return nil } - return decls, nil } func (b *mainDeploymentContextBuilder) getGoType(module *schema.Module, node schema.Node) (gotype optional.Option[goType], isLocal bool, err error) { @@ -1142,6 +1243,7 @@ func (b *mainDeploymentContextBuilder) getGoVerb(nativeName string, verb *schema return goVerb{}, err } return goVerb{ + Name: verb.Name, nativeType: nt, Request: req, Response: resp, @@ -1333,8 +1435,14 @@ var scaffoldFuncs = scaffolder.FuncMap{ return nil }, "getRawSQLQuery": func(verb *schema.Verb) string { - query, _ := verb.GetRawSQLQuery() - return query + query, _ := verb.GetQuery() + return query.Query + }, + "queryRequestResponseType": func(s *schema.Module, typ schema.Type) string { + if arr, ok := typ.(*schema.Array); ok { + return genType(s, arr.Element) + } + return genType(s, typ) }, } @@ -1557,6 +1665,19 @@ func nativeTypeFromQualifiedName(qualifiedName string) (nativeType, error) { }, nil } +func usesType(actual schema.Type, expected schema.Type) bool { + if stdreflect.TypeOf(actual) == stdreflect.TypeOf(expected) { + return true + } + switch t := actual.(type) { + case *schema.Optional: + return usesType(t.Type, expected) + case *schema.Array: + return usesType(t.Element, expected) + } + return false +} + // imports returns a map of import paths to aliases for a module. // - hardcoded for time ("stdtime") // - prefixed with "ftl" for other modules (eg "ftlfoo") diff --git a/go-runtime/compile/queries-template/queries.ftl.go.tmpl b/go-runtime/compile/queries-template/queries.ftl.go.tmpl index 0fa048c99f..9e1c18f413 100644 --- a/go-runtime/compile/queries-template/queries.ftl.go.tmpl +++ b/go-runtime/compile/queries-template/queries.ftl.go.tmpl @@ -1,5 +1,6 @@ {{- with .QueriesCtx -}} {{- $module := .Module -}} +{{- $dbName := .DBName -}} // Code generated by FTL. DO NOT EDIT. package {{$module.Name}} @@ -12,9 +13,7 @@ import ( ) {{ end }} -{{- range .Decls -}} -{{- if is "Data" . }} - +{{- range .Data }} type {{.Name|title}} {{- if .TypeParameters}}[ {{- range $i, $tp := .TypeParameters}} @@ -25,17 +24,33 @@ type {{.Name|title}} {{.Name|title}} {{type $module .Type}} {{- end}} } -{{- else if is "Verb" . }} -{{ if and (eq (type $module .Request) "ftl.Unit") (eq (type $module .Response) "ftl.Unit")}} +{{- end -}} + +{{- range .Verbs }} + {{ if and (eq (type $module .Request) "ftl.Unit") (eq (type $module .Response) "ftl.Unit")}} type {{.Name|title}}Client func(context.Context) error -{{- else if eq (type $module .Request) "ftl.Unit"}} + {{- else if eq (type $module .Request) "ftl.Unit"}} type {{.Name|title}}Client func(context.Context) ({{type $module .Response}}, error) -{{- else if eq (type $module .Response) "ftl.Unit"}} + {{- else if eq (type $module .Response) "ftl.Unit"}} type {{.Name|title}}Client func(context.Context, {{type $module .Request}}) error -{{- else}} + {{- else}} type {{.Name|title}}Client func(context.Context, {{type $module .Request}}) ({{type $module .Response}}, error) -{{- end}} -{{- end}} + {{- end}} {{- end}} +func init() { + reflection.Register( +{{- range .Verbs }} + {{- if and (eq (type $module .Request) "ftl.Unit") (eq (type $module .Response) "ftl.Unit")}} + server.QueryEmpty("{{$module.Name}}", "{{.Name}}", reflection.CommandType{{.CommandType}}, "{{$dbName}}", "{{.RawSQL}}", {{.ParamFields}}, {{.ColToFieldName}}), + {{- else if eq (type $module .Request) "ftl.Unit"}} + server.QuerySource[{{queryRequestResponseType $module .Response}}]("{{$module.Name}}", "{{.Name}}", reflection.CommandType{{.CommandType}}, "{{$dbName}}", "{{.RawSQL}}", {{.ParamFields}}, {{.ColToFieldName}}), + {{- else if eq (type $module .Response) "ftl.Unit"}} + server.QuerySink[{{queryRequestResponseType $module .Request}}]("{{$module.Name}}", "{{.Name}}", reflection.CommandType{{.CommandType}}, "{{$dbName}}", "{{.RawSQL}}", {{.ParamFields}}, {{.ColToFieldName}}), + {{- else}} + server.Query[{{queryRequestResponseType $module .Request}}, {{queryRequestResponseType $module .Response}}]("{{$module.Name}}", "{{.Name}}", reflection.CommandType{{.CommandType}}, "{{$dbName}}", "{{.RawSQL}}", {{.ParamFields}}, {{.ColToFieldName}}), + {{- end}} +{{- end}} + ) +} {{- end -}} diff --git a/go-runtime/compile/testdata/go/echo/go.mod b/go-runtime/compile/testdata/go/echo/go.mod index 22c33bef34..5c5de2218b 100644 --- a/go-runtime/compile/testdata/go/echo/go.mod +++ b/go-runtime/compile/testdata/go/echo/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/compile/testdata/go/echo/go.sum b/go-runtime/compile/testdata/go/echo/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/compile/testdata/go/echo/go.sum +++ b/go-runtime/compile/testdata/go/echo/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/compile/testdata/go/one/go.mod b/go-runtime/compile/testdata/go/one/go.mod index 6e9fe13c80..71e922a20b 100644 --- a/go-runtime/compile/testdata/go/one/go.mod +++ b/go-runtime/compile/testdata/go/one/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/compile/testdata/go/one/go.sum b/go-runtime/compile/testdata/go/one/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/compile/testdata/go/one/go.sum +++ b/go-runtime/compile/testdata/go/one/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/compile/testdata/go/two/go.mod b/go-runtime/compile/testdata/go/two/go.mod index af195a5e9e..d8f26c7e3f 100644 --- a/go-runtime/compile/testdata/go/two/go.mod +++ b/go-runtime/compile/testdata/go/two/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/compile/testdata/go/two/go.sum b/go-runtime/compile/testdata/go/two/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/compile/testdata/go/two/go.sum +++ b/go-runtime/compile/testdata/go/two/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.mod b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.mod index d798cc8b8e..8590059cc2 100644 --- a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.mod +++ b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum +++ b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/ftl/option.go b/go-runtime/ftl/option.go index 8ac8cb712f..2b2fdfd855 100644 --- a/go-runtime/ftl/option.go +++ b/go-runtime/ftl/option.go @@ -223,3 +223,25 @@ func (o *Option[T]) Unmarshal( o.ok = true return nil } + +// SetNull sets the Option to None +func (o *Option[T]) SetNull() { + o.ok = false + var zero T + o.value = zero +} + +// SetValue sets the Option to Some with the given value +func (o *Option[T]) SetValue(v reflect.Value) error { + if !v.Type().AssignableTo(reflect.TypeOf(o.value)) { + return fmt.Errorf("cannot assign %v to Option[%T]", v.Type(), o.value) + } + o.value = v.Interface().(T) //nolint:forcetypeassert + o.ok = true + return nil +} + +// GetType returns the type of the value in the Option +func (o *Option[T]) GetType() reflect.Type { + return reflect.TypeOf(o.value) +} diff --git a/go-runtime/ftl/testdata/go/echo/go.mod b/go-runtime/ftl/testdata/go/echo/go.mod index 047600f0fe..31e4830083 100644 --- a/go-runtime/ftl/testdata/go/echo/go.mod +++ b/go-runtime/ftl/testdata/go/echo/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/ftl/testdata/go/echo/go.sum b/go-runtime/ftl/testdata/go/echo/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/ftl/testdata/go/echo/go.sum +++ b/go-runtime/ftl/testdata/go/echo/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/goplugin/testdata/alpha/go.mod b/go-runtime/goplugin/testdata/alpha/go.mod index 4d8c03dc56..47a1c3be5a 100644 --- a/go-runtime/goplugin/testdata/alpha/go.mod +++ b/go-runtime/goplugin/testdata/alpha/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/goplugin/testdata/alpha/go.sum b/go-runtime/goplugin/testdata/alpha/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/goplugin/testdata/alpha/go.sum +++ b/go-runtime/goplugin/testdata/alpha/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/schema/testdata/one/go.mod b/go-runtime/schema/testdata/one/go.mod index 9c92937eb2..40ec9711d6 100644 --- a/go-runtime/schema/testdata/one/go.mod +++ b/go-runtime/schema/testdata/one/go.mod @@ -36,6 +36,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/schema/testdata/one/go.sum b/go-runtime/schema/testdata/one/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/schema/testdata/one/go.sum +++ b/go-runtime/schema/testdata/one/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/schema/testdata/pubsub/go.mod b/go-runtime/schema/testdata/pubsub/go.mod index 4b5cfe2426..db37076fa7 100644 --- a/go-runtime/schema/testdata/pubsub/go.mod +++ b/go-runtime/schema/testdata/pubsub/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/schema/testdata/pubsub/go.sum b/go-runtime/schema/testdata/pubsub/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/schema/testdata/pubsub/go.sum +++ b/go-runtime/schema/testdata/pubsub/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/schema/testdata/two/go.mod b/go-runtime/schema/testdata/two/go.mod index 68690cb805..8d42dd73eb 100644 --- a/go-runtime/schema/testdata/two/go.mod +++ b/go-runtime/schema/testdata/two/go.mod @@ -38,6 +38,7 @@ require ( github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/go-runtime/schema/testdata/two/go.sum b/go-runtime/schema/testdata/two/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/go-runtime/schema/testdata/two/go.sum +++ b/go-runtime/schema/testdata/two/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/go-runtime/server/query.go b/go-runtime/server/query.go new file mode 100644 index 0000000000..cc5eb0c7bb --- /dev/null +++ b/go-runtime/server/query.go @@ -0,0 +1,135 @@ +package server + +import ( + "context" + "reflect" + "strings" + + "github.com/alecthomas/types/tuple" + "github.com/block/ftl/backend/runner/query" + "github.com/block/ftl/common/reflection" + "github.com/block/ftl/go-runtime/ftl" +) + +func Query[Req, Resp any]( + module string, + verbName string, + commandType reflection.CommandType, + dbName string, + rawSQL string, + fields []string, + colToFieldName []tuple.Pair[string, string], +) reflection.Registree { + return reflection.Query(module, verbName, getQueryFunc[Req, Resp](commandType, dbName, rawSQL, getQueryParamValues(fields), colToFieldName)) +} + +func QueryEmpty( + module string, + verbName string, + commandType reflection.CommandType, + dbName string, + rawSQL string, + fields []string, + colToFieldName []tuple.Pair[string, string], +) reflection.Registree { + return reflection.Query(module, verbName, getQueryFunc[ftl.Unit, ftl.Unit](commandType, dbName, rawSQL, getQueryParamValues(fields), colToFieldName)) +} + +func QuerySource[Resp any]( + module string, + verbName string, + commandType reflection.CommandType, + dbName string, + rawSQL string, + fields []string, + colToFieldName []tuple.Pair[string, string], +) reflection.Registree { + return reflection.Query(module, verbName, getQueryFunc[ftl.Unit, Resp](commandType, dbName, rawSQL, getQueryParamValues(fields), colToFieldName)) +} + +func QuerySink[Req any]( + module string, + verbName string, + commandType reflection.CommandType, + dbName string, + rawSQL string, + fields []string, + colToFieldName []tuple.Pair[string, string], +) reflection.Registree { + return reflection.Query(module, verbName, getQueryFunc[Req, ftl.Unit](commandType, dbName, rawSQL, getQueryParamValues(fields), colToFieldName)) +} + +func getQueryParamValues(fields []string) func(req any) []any { + return func(req any) []any { + reqValue := reflect.ValueOf(req) + if reqValue.Kind() == reflect.Ptr { + reqValue = reqValue.Elem() + } + params := make([]any, 0, len(fields)) + for _, field := range fields { + fieldValue := reqValue.FieldByName(field) + if !fieldValue.IsValid() { + continue + } + value := fieldValue.Interface() + + // handle ftl.Option + if fieldType := fieldValue.Type(); fieldType.Kind() == reflect.Struct && fieldType.PkgPath() == "github.com/block/ftl/go-runtime/ftl" && strings.HasPrefix(fieldType.Name(), "Option") { + getMethod := fieldValue.MethodByName("Get") + results := getMethod.Call(nil) + if !results[1].Bool() { + value = nil + } else { + value = results[0].Interface() + } + } + + params = append(params, value) + } + return params + } +} + +func getQueryFunc[Req, Resp any]( + commandType reflection.CommandType, + dbName string, + rawSQL string, + paramsFn func(req any) []any, + colToFieldName []tuple.Pair[string, string], +) any { + var fn any + switch commandType { + case reflection.CommandTypeExec: + if reflect.TypeFor[Req]() != reflect.TypeFor[ftl.Unit]() { + fn = func(ctx context.Context, req Req) error { + params := paramsFn(req) + return query.Exec[Req](ctx, dbName, rawSQL, params, colToFieldName) + } + } else { + fn = func(ctx context.Context) error { + return query.Exec[Req](ctx, dbName, rawSQL, []any{}, colToFieldName) + } + } + case reflection.CommandTypeOne: + if reflect.TypeFor[Req]() != reflect.TypeFor[ftl.Unit]() { + fn = func(ctx context.Context, req Req) (Resp, error) { + return query.One[Req, Resp](ctx, dbName, rawSQL, paramsFn(req), colToFieldName) + } + } else { + fn = func(ctx context.Context) (Resp, error) { + return query.One[Req, Resp](ctx, dbName, rawSQL, []any{}, colToFieldName) + } + } + case reflection.CommandTypeMany: + if reflect.TypeFor[Req]() != reflect.TypeFor[ftl.Unit]() { + fn = func(ctx context.Context, req Req) ([]Resp, error) { + return query.Many[Req, Resp](ctx, dbName, rawSQL, paramsFn(req), colToFieldName) + } + } else { + fn = func(ctx context.Context) ([]Resp, error) { + return query.Many[Req, Resp](ctx, dbName, rawSQL, []any{}, colToFieldName) + } + } + } + return fn +} diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index 38708d301b..65e241a73c 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -75,8 +75,8 @@ type Handler struct { fn func(ctx context.Context, req []byte, metadata map[internal.MetadataKey]string) ([]byte, error) } -func HandleCall[Req, Resp any](verb any) Handler { - ref := reflection.FuncRef(verb) +func HandleCall[Req, Resp any](module string, verb string) Handler { + ref := reflection.Ref{Module: module, Name: verb} return Handler{ ref: ref, fn: func(ctx context.Context, reqdata []byte, metadata map[internal.MetadataKey]string) ([]byte, error) { @@ -106,16 +106,16 @@ func HandleCall[Req, Resp any](verb any) Handler { } } -func HandleSink[Req any](verb any) Handler { - return HandleCall[Req, ftl.Unit](verb) +func HandleSink[Req any](module string, verb string) Handler { + return HandleCall[Req, ftl.Unit](module, verb) } -func HandleSource[Resp any](verb any) Handler { - return HandleCall[ftl.Unit, Resp](verb) +func HandleSource[Resp any](module string, verb string) Handler { + return HandleCall[ftl.Unit, Resp](module, verb) } -func HandleEmpty(verb any) Handler { - return HandleCall[ftl.Unit, ftl.Unit](verb) +func HandleEmpty(module string, verb string) Handler { + return HandleCall[ftl.Unit, ftl.Unit](module, verb) } func InvokeVerb[Req, Resp any](ref reflection.Ref) func(ctx context.Context, req Req) (resp Resp, err error) { @@ -138,7 +138,7 @@ func InvokeVerb[Req, Resp any](ref reflection.Ref) func(ctx context.Context, req } resp, ok := respValue.(Resp) if !ok { - return resp, fmt.Errorf("unexpected response type from verb %s: %T", ref, resp) + return resp, fmt.Errorf("unexpected response type from verb %s: %T, expected %T", ref, resp, reflect.New(reflect.TypeFor[Resp]()).Interface()) } return resp, err } diff --git a/go.mod b/go.mod index 0ab9142d0d..2fed612812 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/lni/dragonboat/v4 v4.0.0-20240618143154-6a1623140f27 github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-sqlite3 v1.14.24 github.com/multiformats/go-base36 v0.2.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 @@ -252,7 +253,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.10.9 github.com/pelletier/go-toml v1.9.5 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect diff --git a/internal/buildengine/testdata/alpha/go.mod b/internal/buildengine/testdata/alpha/go.mod index 4d8c03dc56..47a1c3be5a 100644 --- a/internal/buildengine/testdata/alpha/go.mod +++ b/internal/buildengine/testdata/alpha/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/internal/buildengine/testdata/alpha/go.sum b/internal/buildengine/testdata/alpha/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/internal/buildengine/testdata/alpha/go.sum +++ b/internal/buildengine/testdata/alpha/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/internal/buildengine/testdata/alpha/types.ftl.go b/internal/buildengine/testdata/alpha/types.ftl.go index fa48887343..49c977dbfb 100644 --- a/internal/buildengine/testdata/alpha/types.ftl.go +++ b/internal/buildengine/testdata/alpha/types.ftl.go @@ -14,6 +14,7 @@ type EchoClient func(context.Context, EchoRequest) (EchoResponse, error) func init() { reflection.Register( reflection.ExternalType(*new(lib.AnotherNonFTLType)), + reflection.ProvideResourcesForVerb( Echo, server.VerbClient[ftlother.EchoClient, ftlother.EchoRequest, ftlother.EchoResponse](), diff --git a/internal/buildengine/testdata/another/types.ftl.go b/internal/buildengine/testdata/another/types.ftl.go index 88f98a97e0..a418cd138f 100644 --- a/internal/buildengine/testdata/another/types.ftl.go +++ b/internal/buildengine/testdata/another/types.ftl.go @@ -20,6 +20,7 @@ func init() { *new(B), ), reflection.ExternalType(*new(lib.AnotherNonFTLType)), + reflection.ProvideResourcesForVerb( Echo, ), diff --git a/internal/buildengine/testdata/other/types.ftl.go b/internal/buildengine/testdata/other/types.ftl.go index 66596779cd..39d9928126 100644 --- a/internal/buildengine/testdata/other/types.ftl.go +++ b/internal/buildengine/testdata/other/types.ftl.go @@ -30,6 +30,7 @@ func init() { ), reflection.ExternalType(*new(lib.NonFTLType)), reflection.ExternalType(*new(lib.AnotherNonFTLType)), + reflection.ProvideResourcesForVerb( Echo, ), diff --git a/internal/projectconfig/testdata/go/validateconfig/go.mod b/internal/projectconfig/testdata/go/validateconfig/go.mod index fba3eb1942..418e7169e4 100644 --- a/internal/projectconfig/testdata/go/validateconfig/go.mod +++ b/internal/projectconfig/testdata/go/validateconfig/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/internal/projectconfig/testdata/go/validateconfig/go.sum b/internal/projectconfig/testdata/go/validateconfig/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/internal/projectconfig/testdata/go/validateconfig/go.sum +++ b/internal/projectconfig/testdata/go/validateconfig/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/internal/sqlc/resources/sqlc-gen-ftl.wasm b/internal/sqlc/resources/sqlc-gen-ftl.wasm index 98e812c14357e6485ad7742109266ed8bc1aae10..12cd4b9c351f4da675cb07461f4acab84e01ceac 100644 GIT binary patch delta 36376 zcmchA349bq_J3D(&ymbb(u4#;fY37p2nd3TfQUk8m%{^ZT~}Qd1eJiCS;bw~i4qhg zYA{G4iy9^TKt%)r2Nf4Y#E20Ba;XSGKv6(IQ9)4u-&fT=2L%7S`}zI)+LSXMX8M#xVf@-u68)$CwC?qCuIrq6e7c8eTxXhQ z7(O5K$^Sft--G|skmWIa=_o)EqeA-SRKkmnN?!ahjp>G=1sK;DXQ=K87#=h*yk3t# z==b|QexG4*hBu!P#cxBt`TZPezY+9%&@^BeUdBR<`2#*5^}_)oV>;8-e;gxVAcnpT zo)Iz(10yiTjSw;o&BzR(DVpIwK*T(TC*Uzrg#Y{|=N|6Yj7E*JvWl~^0^EZ>Pz(Qa z!{ad+It%!b<1;*5!$0*OAwm5o(;jq!|Ck5eaj)UQ?6}v9F)?j5Fopt<%!?xCVMgVv zd>nhHvbEke?=yaot>Y{Ca(;;2`4m6QHuF#U7QTmnRe76!XLKw7k^jWk^X+^Gujd>1 z$9yCIfgj@g`2qedKgbXB@A&up2>*hA!qa!G3qGxLEpQcMQQn95Y5hQHX{lx}owce| zD=y?(NK47FSvRhk6RD2*&V%?J*38Ez;Av?gmKo5rfTg3na-h+y=?N+i@IIDS$b)@D zs8YyH^Mt7L^PGq(v^2X{u{nvlQ}qg*dWGhR%Ed+}zp56vTD;@skuy%yqa25N&vjb2 zFi$M%=E-1eN@ZVfM_&GU;>g>-qG*GY=1$!DQ=$(Fb_7@Fx^*<;$&`^s|;Ogw}8!9M4 z!2=&RF!*OEc&wp z>g*K_6_laiqlOChpSQ)a6ujN!wD5M}_1h}zn_kAAS=2dq7*na1 zm9b_|r+~QVro1tXX1K0-9)Y?pd~@YRE!yyHpH_};age{asd7-uD#A%Lzinmv{A&m= z(fsageq}7bWzMdRK!=@eP9Zr8>%p{u{mQ1wuk(vJ9@n<&jE)yQ)T%#MJs)PzRK64G ztP+1&bb0FtfwgE{LF<-m$|7M8%VP@{v70XO;xTaWhnMC(K=K{dY|Y{jWJ_xv)`Bcx zX~68+uU^d`m{9rftNAUa$XYtGrIcEF*duFMy7^n>JFj+NM;GmXb##E}bn{!C@L2TN zTMKn|qO$k$-WWH!ye-64jOobQE~;H|3(<2?rQ|G21QQoXei94DGv@5HV?AYpAGs z`@$iGGmpiAt|u3Lx-VZc^w0bA*|ExI2mXbITMs-+O@@3+O(uQ2jxXO``R9XSl>GhR z<@u_;gZqHRY_qrkz?Ev50ekuGMSBlk!Q`-KFB*Qhx$OJb@6JR=vG4j)uPV{$`w8jG zwn>en1wN>J=g9f8>+ z-;bZMXzj6ijGb8Y_v62%1L)X&UY17?i0M8y79FgXQ@2vmw85S1>*}3Q|8e|>tcqzzwpVU=rVTagV{GsDw zL5MZsWyeKLh;_)>{S?%Sxe;rmTszzGfse(dZCG=5B6epRc8~07XIr*hNB;92*n)tfPvC3LbWxSXnuyx&tWeB2gKb8S zkgv@=uYzQ?Z&;_r88V$s%O;+=SeyrSW;yJHxTiDg!79Xp&a4Si$GWgC;@eIjLT(o} z=-+7mVwa@mF*Lu%2)W{5pJ3)Oal^R`>PCz|mwoIAip|10Wa^msb2rvGSE&;Y0GVSw z;7Qg)RX0|~r|%JMyR&;Sp(;#{P{OwHwy}df*j2zotf&{eHblJk^hK<<e}VV9U(O0J$W50s$dxF) zoDDpQ_pDgt3I=WXb?l-mVU1?j*GrKMLd8f6MYrqg#m(2U$NA!e;@fLk4A0n{>sZXg zKie;Ey@~C?GuHnvY&=nN#aNb;B(t#{W7$`XKm1*+ri4}L#I42S*>#E4$N&Xu$Fr`9 z94Idlyq8^v&ThMx{gpi_KDig-^Q6f98)Ws#*o40UgH0W^>H(AiJ9k8}<;QPxiumI1 zYzpRCav%E(9vkjs;}e`vqoeCT{ewjkgJmOU;y>8UKBdu^sC$>SilyDpa*V7>V%t0m zylUqb+ogW4z%T?5#)RInyf#ebriAJ%L?&?tbd68=sCk zMMigCg{M6oV;zyBtXHF$>zvUHq8kwL`2^O6@82hkGBzB-JGzWT8AR;RMD`~n2Ry_s zf{GSTKFrz!r^_D3yj+}yERU5Taq3k#Jurzqg}M_S0q;KG?#>vnx zQ({fa*+!N?^>jk@(Z|?cP;uwu3@eJLdV*p35G$Wx)A6{mf?a^e(-q9ZWq^G?? z%pNOvg-J_ITs5Da15h8D4?a2(TQ;A?(0#0A0m~xseRvV;L#wy=;0=aFDt7b@_F_7v zXRTtbbChN-)I_pBVm1ZydR2-kmzshx*aw}(en4$A@>Ntdse}0NzMRU4r8lRw+ zZ!FueAofxx3W__aF7PW3PUB}N69IOHU07@`6YT_V$LD=2t`I!CWvv{3k7BrNv>|86 zpkID-qj;$w?@A-p_v55-jQ+fUk{$ZXYv4( z<)N2N6~^}qG4V|izp(Fg^1Cbp>N(<3McsI=xJ@-XLsja;Zt=f zwhY@UhG0r5Lp5>NA9ya-;Q$IcV!bn;R=v6%jz+#5wi_RCaxHuNhd?{gOA@V%17F0` zJnW4_?*R@`79It9-drY|uhL~kU?H&^2rwph}6Oqgg-IZhP%}?Wzz9Qm(y=#<$Z|M}4VtBe^H0dcw+js{A^f2bh^y+x`Vbv)=Jr1N>+4&>@J>1)1Pr_)}eKTk$A0a)cWz%tOq3CQfe(bDa(F@ zgfxtG*si6bXrj|BTIqM0m^U}w2AX@RVDr)C{KHEXGyNW5XL%Y*3PBME(0 z9RH0KrQViEGu2R`%wQ85{}I_uBX|59WL2G-^|0(QUW;b4BQ>iLvVM>gjMr)!4K}7B zK9b`)GT%2u_+2B1h-cBL+f%b>h+WB9AU*X)%=yiiZ+{PV#E_KwV(Z(LNTC6xcCTFj zGMG~jngKg1wU?yFJPBB-II?EvEMLEnGvRMn$}%VQ!#A^M zp{A4CRX4FRQS<97>%X|8(N6)vwHm9nr1~ zHA>)ziVm!NZ1U>zr{CytWyq&EXrcsMcRMzd1nmRazf%Z-|z=bC! z1)hfMofxodiwiKc+!FIs{W?l$0oxDmtrNoq9~Av0z;awWxSkD@PNB51>Bz z3Eg-Ba>B)79fQG$OWHpnJOxl#NI=SF3fPE&a}%WB)rujEyttF5_0*^{-k$r(O6IoF zQ;VWI8=hq^ix&~7CRxqc66~yL=WDiYggrJ3d+`KMh5fb`PK%qln5#b`oeP>Zj5xs6 z5gRI6IH>x(!s&B`)}0SPr`W#U3ggiVbmJTGw9TGcU6fuPD4kS_Db`XUD%Cj8RaC}% zYAf(8LLW;h4OqznrRQAdSw&BDHj|!CU(@kUeNA=hz{iw*mE%c$l{xuQ*u>OViF%As z;BBMQjX)f?_iM%Gav}roL;q-!H$_79xD$#gS{z{mL3bpt31`sb)^H|0hD1zkiRvZr zI>2Iu{Q5{Eq&cOv;&2u?sKtnIV@tmg+os`c;5P?&Bm6ee`&b^UNhED}*axSM6^ENx zCSK50*lRV04W1n~cpuaa>;(xNzQSYFE~N-KGjC}|pvNJ|VF zu(BizF?)1z#0NgI(l7%nbUEgb01(@BsE1K3RLO;x+CUWpi-6_7DC9>iKYFHzl}`Nx zEPruFtU$}77sI$GWsHLUA*oRTFV)oDnz~c-QkXgLWspx5sb}T8f|CmQfc}JG$WO8P zq7sNgerY%-VC7gr!b=2`rn3}m17T>%^(X+;!EXGDDDdPDSWPTXKUhavfLz{?o$5WO zLB%YS4(&VCzdS*7=9F&viN2|G!rYM$YbziNfOI5>ON1$aBQV7rkfRBh2ZX&+NG^@| zB;XN=dx)t4m~?{MCyC<&4gJOa&_f!YprsF#Mv>kJs3Td6q&d1qBO}jBQ zfQx!SE5nYCFOJ}h8S~aHpF(OTwZ_J#(j4H@yGg3849afgW)sKB?SnM?H~gmo3*b&& zdZB&6iD`YqMEW$DokofR1WZ!lhGa#3bBLnm0;^yrm|c*i z_}S3fR+?EQNiRihH24vGQ33~>*_t_3iKpcU{2mv6^e#6xq8gw?1neGh8q1Y2U~&L%6d6bpF|dRMO|l>h;A zndID)Z1kpJj9-(@_7K)ZoAnQwkX{&!p@eP5Y>-lQ&f=ZN4MvC;83?eF+;JTP!5eJu zAj)wrFfl^gu{Cft`iEfl(m;Jfpo~VtI zBN6wXOypIkx>T779Hx3(@S!uPk0YsjL9ROioZGL+uHxH78NnRa`lQkrl_q)V0Zb@q zuvCY+F*#8Z501y_q@2x>G*z}|+opYRFfvt2dZXlyDYjECUV35(!fI6Rr;t*Xh&##G zNrvYWIT_d2X+-{qak6*q5R(|r-X`nE9Tu;`c*=%K-;gJo04LQ$3}w26hC-8!6!r!K zNe1t~#a<0?S1_fIXxykhJ&!if2^6_x_axtjCG6w>psT2t2rK(a^0k;9_YVGpzP#{n zp;MK3Xm%M%ACQ*p$1s&k0?EDOFTkT*ByzDG48vDaTW;P!%A!jK zDh;W8CwZXIFg?k4ipqEuXhl`qM+@}n!N8EGdN3d}EsUCl_@!h$jvSvfMrh5A`$MqN z9d{^jCPKeDB#Z^Hj%NFv*aihQf(ZOL-%$=z?C1B{A2@qPT)2i^h0~QOYjEC#2okaB zL$(kxDx%L8c8%Cp%eo@WMr3`26WC{CSAE31jM6u(W%p-4<%ZY5Fp>tB{n&c3e=Yk{ z_RNM#{030T78L-+q0)Xtsg;=(!6MKvrytauAr3TG5v(eeFxeb`DEM0s4HZkQ39D^^~%9fY_eB5y~rZVUTkt2(M~n@-#jm)mMKOy?so_w4e|#gtFj2=VMz7D{eb zm$LPH^v#&snEa5F%%KGD2ExKM5fsx*ayw z^WypKApRzb<)S!5W@jp9tKKLMY-f4=`3FU(JviBU^;_o4e!rn=*X#gvZ#7U7TfT!K zIu#RW`W}eW LX$9Az1+0zqnN|%Y*Pi!PsY%I!mv#r_BHdH)*4{Xd?;>|s* z1r9Q|>|yzFcDVJ^ld(GgOV&1-e+os1uTaP3pU1vp&01EIQUN<6v;;DAR72Q;ZdZ7U z@ro1scfVq9W&hd`exBS5Ed1C|$uE1QAh-A$wEVWA3U_`@P%Gi^K#|vN6UV-0f6m_9 zQ03ddVVw}YC!YF-S{}Qp8xV2#1NbiUG}jX@R+=h zwM%KU)ortl+I+Z=H3u9|J;0}mx(#5dB)yp6Jh7j(W3$Eb{cJW-@b&|26mia$1FR$w z=gO4!VfJV1#Vy|gGhZhp#L<{2SwkgBJe4Rp_aOV6de1(@22t<#9Aft*_l^aP`0~LG zf*(%pBT;hKVMH7#2ABHr?(f)&>}98LV{C)C_N=EV-&YL z!t7rE6X%$tIDoZ#NsgIr6x+R={YQAIT&_MR==dj9)5(?bZN^g-qPX#gBu;(x2X=0p zNqhdta$6*Z+zU_J8FHI0hZG}!Wbe?t&-zKpR-+TFxp?^}FjNw%LkfW}Y!JCefxx8M zB}yh7g>20c?;S-ykP#^5>xH$QrKO6{XU70)D)*NB%q%y|0SsB@a@i#E{za>ASjXnz zryvMenIK1ka3Fqb@#2rHk!W|EJ#x}i?n$17vtqrvLCicMg$xSwEK#)+yt^pTbE1~- zm(;l?O2+0FM zf6;~U?x~cNZ0IUOi&JDwic(t2d8*3UOWTEVPS~0khp|fcdXZ456w(n{8V?2{Mli53 zgy$r~d?`_{R3e0eT5O$XH-4E)s8+d~uUJN@q~i_gkymvtb>eoNrz)^`Gp-0J9id`Cxwd{@dD?#I#)Z=_9)UC-Npz3<7cYpOX?zA^f?}J~__;D( zG8EwNWT{9iKO$nROjxkAB-RIbTVTx>EZz-|a+R8!#s6?#@?tDu2Ho9VTqP0mG755W8AZ+nbSl6hk|=1* zuSI8nYs@<*ri6L}9Pzpo%(J2~){yvs`2eh$D{C5JAD#0pY%n6|PFzmKu83(EB(=Dd zilv+ZE@G$DVma$t!6_hJ^qW#Eg0XxKPYf$O~r3dTz|2jDcGu3qq$W2+D$_E(`n0Xaz8a1Sc3s9y%3zid>M;PbuRC z4GfwlnO4O6Zu6hXdWCEIwbMna8uEA!9?B$oot@S zBPtR$=#^u-a;q>PIy@xDy8I!YlcI{sq?Q6elStG4;~|LSV)05jFGz)1cVVtm{hUfi zsR1P&A5Q2bXYebKb!LLJx2i2X+8dV@7Avj?BNs!FVrv-39%9E#h}U9K@w9|{$hYiM&^QPMEVTgl`RpyXYe*b)iSUKm-nKT?1pxgpAAnJ&(GlP zfw?DU@*Gh=g9q_`U>W();5{?+esB_1f+CdR4$n z@Oy13nwCJr?x$uhmat*z2UVqR%U0l$qFOi;C#y>mv-oT#zt|(2isv8Z z<544Tl2m21TsgPy02o+fI?_D6X;JRX0PQ@C>Nqx>9pV%lWh5{dlDi1S?{?8!Wjy(Vs&EbISuGA~5g zv&hVTjqpSeU5_nii{=GH)=rkZcz7~z6?#Keunz$hZ-|!V5_6Z8pR~}|xze?fr0f22 zo|hBvc^k@bM5ekXiNn}QSV5{o1r**A(c>}R8>#V+(FDZ2$K(vwJjVZwRKepAm?fg` z;}Z0dkMj=5eEM;@7A;Ykj>M*Reen~#1=_WOV4^|!6TEkm1QqUc^bbz75CZ;$oZ2lt zI6004QkOW*t0Axs&F3;ja~E$_k^`ODk^G}^DhST((0^XLI8r7_gsEnVRg<-X=47WNuvyk1OyqBXM7cxKuPj^F9o3#LP)^of=t2(M4;;3feyto^e zX{^{&X=m45A#Qw*cWylOGwCasmR7{NAAB^1S=b=jDyN-Br~H&rCws?B?JUvxdEPZTl{O5Zu@F*( zr%`Nfm2<>-W`*p=i$Rc>$|KmGvygyu*n&>w5wTQkd6h4Z^IVO>Q);Pq)PcKJz!BqZn0nRI{s$;_mJ{b_SeRdQ=#6(r#Obgn@u7$hrX{ngpe| zFkj#uhqM3$2uI)If^1Pt1s27K7w6J68@gzSBNE(&0Z!a_LRDG*>R;0sm%Q+!D@ zgt6QslpxF@EnI}F`85NlW9n1}DpeRZLlM%5ON+BqI)-e3r9dNUmS8CaJhc$Zs(C>> z=i(BMbW}GK_(q6PlgE|=S`zh***r3k#!(ecDlRrbD0K$Ho!A*kcY>V`i-@-)96>+! zEV?mfx3_ES#i-fvZ3@Igv*CfQm?bvP=B<=)EK`bv2u+%VIKiGq{U?}{LkTHtz(gfJ zbte8Haf4Z_cmjfpP&P+!C36z)AVM>#T#;VF=3Hm!ABqyMPlap}whZ%2;v}L1p}=8q zdpC=7xSEztbC-q+YY!ZUX@eX=c0t*I(~G?|(Z=$3=N%Q<3Q-YrDyMl;AEgXew#p3g zY&~mo<@HJ3+FKC|CfPxf*Iv7ZkZ78Zt9ekQ2l&CDSRx6yQ0{q5UB!?)_vOCPJ@aK>(AGUQup6X+UuGq%=IiMrHH_TbAM} zsYo@~5oy3&a&k29_Iha=P|sfpry-0LX#yLh07;B6S}O3J!+b;&pwBdT{M0unBFDg( zatxfVD2bxD2@;1SJ`^Am@@l1)wrjizJ_Je;l8Ah}3h|W^Qu8sYjrST~v2BdSOB7}1 zn?yDR5m{satt`#)`~bQ4l_KjQ%%d#qms&+8y)ubXU@snMgc;!50}wCFK(Fyx{y7mR zc0yj#!PIMg=mbRc7g>|$gn^X-Di4rg64e-67G21z)=IG!&qW5+At95@iS<%y zUQ81u72@Vx-ZTov1Ir+ugC>coVfn@jF_&4~UUwHA$^(wz2W3@rP z7>*5$h>yYp@j4UHV$jW)GexD^_dw;^O)o*nH~6B&u0&{%fiN3HFcM3_l_N(EQ-Uh1 zCKj-t^Z}es+AKc>w1Ns=2av__OJ2}!oweuv=d%hzBQQ3PPzfS-(yIK3&;$_Geu_}T z&Za*cRFGDv4Z%VVb>*0QRwiMAh`)`-wClWNozjUfjey?I*FI1qp!e>4qkJd$Ft484 zhm=GRqNF8O{aQ+A_S8VrOnEg-#cu|&eCc5m7%|EF3G=9Bc4klNkqkRH0@4-@dq}D2 z__2JzUn4IwVR-}?&IS}q(~846c-9&e^ULgN16Xg07p(lyGGPSb+N)63%=U*#OY6iC z>YtT~%_Aoko0JlzP5PB>Rw|`rLkW^Hw5Jn8D1}IiFeyrVx>A1Cgh46qj*^Z_)O4fk z#?4RwIaiubE)nUk6jiYMBXXM9BY=|f+D8#7>bnFf4BZv{r-gk4AO%|j=scu}X_D3r zKdm+4JW$sS|3D-@wim)CWk<>QS_;1QT6v@~WM8xesfZ_6G&5Bs35T|Q}u zO(Yw*HSmmc2-bj8IfOJeF*=3-uKw%|dPsgHT}C|N>N2P&=}^p*d5ULh)k+Lq z2)0*x4Z}hO5cF>?$xB`gv7{Is~bHuD6jLG$r(OMbD zSX82{p3n-NiQh>mOGZ_#SDAQq-Z-XelyR7wJlR}1*&1|5o(uWx5?L)($EnDwp!U&X zs{w3P$PR$57Surv77T`kS?)rS^49HFQt$x81ThF8n$$`L55;Dj2zf#Sf`Wt z6snl^tY8k1{fXN*WV3M8gvlz+{`7u8iEHBH2C&*-fQMnHqmIw~>;dS1P!FaIh53Vx zVb9Rs2>A^#E}EYhNAa}1LofnK9E-f+aFs})uTJxJ-bf^t8?eI?4$<|xp@ zBVJqpqw8^(61QTyw6IB5kzZ)@cG8&LdyLF%H71PXc$D5ki~@HHH}P<-1+B;vf2*6A3x-dx@&8tJ?9(I#ot_RtZGtkcNiTC(LJ%1bZ5AeZa2bg?7A6 zVW!>cVj7zSFVk*y#j6pFKpq>fmElEFO-wB|&J4^)xSh#>Tn4$%7kq9XN!f9`TLD3H; zUjUE-j?N9k_{N33;^DN{PNPbC;Lw}y9%^ts5NV=NjSF)Sb)t}0aWV-mm{xknY8MS( zLV=YIRwfVIVP$U;E5mKW++9}2DGRZ;%gGQHw33``fN;dgRN3WZxALi+OtlmzlPZi# zka~i%pgJIzN><6mfToiO#yChR%(g)l52%}hS?bWmY#U&fhuaX*+Qbnn_ww2RZ!l`y z?L5%YieNd4T8@WIj?)n=l$QFUPSObQhSV(s>D_sYGU?a?i&1;M9{VzWV8L1Da%C>x zh^@O8c*WAw3y&~eFYuCA)D;gX-!6DyH>e{Xo^DYbwR_bL6^s`c91-s9QF(C&4wyEo zT-;^F8c3RYuG}z+kgo=KlT8%(@kapBTw3!hq`ej)V_VsM?#>s%eF&c%qP->91h7)1 zwU=z`m8!PsC{bo#eC$JNYUAl4Ek&>NUIRD|5gc*TD|vvWsvBC^qtaH2 z?@QTLYHX7(@2Uw?cNOSvsn^QEY;glz?crg!K~Vrm!7HI%uOhNS?ay<4!v#scA@a+y zjYs9mLnJpK>UfB9(`Awop^q<+e;DCKI?acIPUb4brD?$y17$ zSWuG0Ki$bzMljgH);eob$x1(8BmU6ZYvq*cj+6-t7Q^6N#(b6fLIw;^KlT|R_l9MP zE6?T4ikvfNT$nf2kv}-{kgmLtBf7biT5`$9`(>1<;~$WI1c|UO=ojcRi#XF4d6Sj6>M8W0R|0sFVE?37a~O;C<9T0`ibALYrq9 zTw_?>Y5N%dGK`;pe$E7q~UmESDxj5XZzaw-R*}U6ouX312nI zwN&U3s#EBYoS+(D+i4otr&inR!nZ#){|M_FRdhhKasqK>(AL;H;Z7(l-b>|{242cs zA*32ODy5Ljc?m&9K%*q96D;X)gbZOsRRjbk!QMyKy&m7#OUb^}>Een+t{mJ1dvlWg zbTWQ0eW?>nI~&#<<>2%MVG?+Q?F4mHQn^0b&Y;)IfzdP4aD<8dIJmc!>o3a2Sqy6A zk{OuzPJt_Ai6``i4j<34zR*>sG;3+JOc=yTx{-y`Vf(qp7r=$NELE?Rd1xf^y)Af) zDy*T%f!)&S>zrYDr_N4fI>oP$;>5m$RG;WmY7&|je$oL1b~6E#u+Qg_v~)r+EQD+^ za5cm7MXNTZMIR1HFMcKLx6_p+3AfH)RLZV_RmdQ&b}1Wsx{*-PHwi)yYZ(Yro)0zZ zbsESB0@@tLu^%qut4&l|CewKC-h;J3P*a|68!5{}J2-6nF~YgEMI} z3GxrK2)hevV}WRZwL|hrlKr1@*C+IHE zR@50clqq@1if?Hb1qssS#PH+`;5CWov4qCU%%n1xb9Ew+E-i<&Iass&bE5D(oLF*j zqg27zaiNcKNk~9cw^SW#NzLsA0g00*Rj!t!+}!8mfi;lA)qk zhICs4!?61zZBaR@BG)2mR+{C%GQ#7bY-t#p2oI65+Pa}^&XPtA^?Hhwx{`J!I%)gq z9S5dZP*iZ2%&=fLNuGg43o0NgN*;%&1R+K3-;F||c^2}$)P028QOp#mBwvu!%F%-` zUF^2@vO{>9Z_vu`f-8X|&>_dNwd&{)bwnWBxCAZ}qIM1fFLm+597MAQM4P#YSq_N) z>M=|`CacFQ>ak8e4$tL}Fhl(PMUI2h?wN+h>}qRUIXS^6eJbo8p-Iz*gHAh2%JEQnhy*s*5{F9v*Fg^u z0#VeRJuspbRJYS3TH_m8fJyD<*vobjT&v~Aq&x*ilna^U^7~0rYga)V#I3WsJ*e&k zn!pk#jV|%LmGVvJtIfpB3IhN;f}?RdbH}+4v_VMjmonlWNj$i`if;X6qlU@mk){(V zCLX~%$w(c=&mm$&&Vn`^X$#ffdO$onk6(n?7_o64k3{2m3(0T&q@X91jGhoSUL6F{ z=8l?^i{X&MFgOtV!xX(l!U9OiAcS+9=Ad2@j5aXk0zjUDW7S>yuwM1U zts~;Qup9!&_}yiV7G)`IAuLBU4k>!zFapmmu|O#cR_L$sVI0fG$k+He(dj8GfK;vY zK|T1rq(l$>C>1MslfH%z58Ph5D@mmy4akz}FZliX_EuWL&1q`AG2eTzR9K5}P2ow= zV-deG>UcMfg+)rhYiVTZVGlD^7bGbG5Fswuj%j9VMI1FyT#1K{5-zw3$bM}AC}C* z7cb#wHC^$oY=qB8QzHv3guj7M^R0Mt2_NBJC3AhhK$>PgM2ox@_(TMj^6(up{2l_J z>C6=X6_GFZi@*eV6v^%E48h3+%?M(r4q0h+YT`wxhVx35pl^4{SbV|)1p$laU*}z# zIt-CIzr6>=-q(3+zO7z_V!S9^O3R@7(1jz(I6+n$$B5@9Ngf@;<-2-fy>8f4W<|I= z+7(r%i8(QTUcn*Blg_e@>^jX+fq_iT+(mYUxmy@Zd0|vacJGe+yG-DkkWMY)W#Nnf zRE9ij#<2)KzW1a!{06@T_cX73lMl;P_CyBR6L18`p1@T^vL`mY$zy!kPBHl{Ucx6; z3$~26h{kCng_Wcdyth|MoP$e6d1v}KnAhFuW40$xoU{*)QPoW2^Y5vYmqLY9No1E; zL?1D88NbElZLq8Gyv^@)rXatmgcUv2Bx)_@zRiauHHkI(7w#al4dR03ycvnh^~>cI zs`BM@F)X%tId9DM+MO#!xG~EW&Bw78eACWpEAdg8`kkW5O1{?jH5rb!Ar6nk*G5WL za&HEwiD3E0fwO&Jr^tGjFLi*T^C#fdOF+@xHQ@D&clnEa>3;FpD%?;}>xAc|b;7G- zzl_!i4(;uwbpi{9yH3y^ruWJSlk0?F?*ZPW)uPFJ{Eyj=-$UPs$95-)xVc7*e2+)c z6Ml`D@gDERtG^Ii-%F?ogvg3ZR`aXbd*Zp(yyJM<&?3noJ z1D@aLVbV$=8AC(*cGXg+@Hif>v*RE1)8Umb}OAz3^Cs)DClt5Lmz>-50BC#qGlbxp;5f%^;GjF^L=qn z9p+klMEs+U-`M(;V%ZT{zgR>GSd4tUj$ddxKwUzN+)0~TEiPKmTbr@n5TNetehjF! z*B0&;_pRr1&MYU|Mremp+Bl$=%uvU*lm#tDo5<7??wVh@0rEOU+`B;%v}yw`ZB7wy z;bF@q9wrI}#nANBpVC1z1~@=1|29XVF3lbV7f=^r7AVjhQS}g}k9j5E`n`DhV}5&M zd5+8SHi}}6PwGO#Jz`+PYHG8{UQ-246sSp zy;v>gZscwGr0+!CMn0keL=W7=+h4y|G7%^>p74L4Ln#d7q1Yh&K7sGIVC%WTYHCYz z9<3X4SS+nF0b^2(-VA;?`ki=aGY{v-$(YO!(kGFCw%*KJx86iU$UNdFbP1P)=u2<- zM=U6YZN{fGrv4y1&1sS@XYt(;Vt-sJtty&w(uT|mwzEz zeZogKb~?k#fCEhQLU%l0_yk{;DE(e+_=G=86~}(cdm|lN^eG?c{5h~c6_$TNbY$iy zP$F8OF`0Sa5qh4R#fE;w?9EX z+ES>P%7WMJ^deg!1XU4_RL@a|j@Y_7;=CpeFr1*7t{PMANr~4ode!00*cG$!R&0&H zAEuFS)CT$wB`2syxxD~suqUZ{*leKGbd@sfa@9y?W1n4>R=K59RjJdZ({|=;5#G*o zZ^Fg8AGY03a)uQzd-j8pq7Q=V& z&LI~Mn0DE0F=vPBv}OmtJVO=HB`mmQ)uK&34^#62^}JVyy$(#6^)`CNA5@@fTj`zl z^O%)>C$4bVWpl**dL9K?MdzJ-A}Wa2ck=5v@-x2RtLVA$3qFXz>Anl!i$}e|yZERq z*=YnZ(2`%|_`7!T+aod`^8tn=2$(Q^xEfzgRh(UqY3;_>93-Mp55z&%%-y`z&60B+ zXK67Z8Hcl^js9;EfZKO*2{=9fi)MTH+14uRHx5iWrQ}Jb0Kv-TdR~*~Jd!3Q!pK>MFgQ0MS1YR8& z@{!Aa34Ki1Yfcikeu)e1lf=X?c@MsLu2}vhZ{14vt~Qye8FuCktV#Hr5m+gQqYe9t zpWEI65DY5c0#=&+t9$t(&6Hh)!MLb?%UArYei90T0gNl(ImscwlkzF|`)F%cra&|G zkII#bqH_nD{pPQDm#EYQ>LeIqdAYnYNR*fPfRfY*g{`|<07b4gNzxhUTarGZP*AUm zNTU;Ij*4hNL;RVT`Tv=UXexf#%iFa&y|{yQ_g9Pazvkx?3y=Srx4!nI$`C$RH(*MR zZop2S-2>VLybaV#l_K$lZ3JK|jy(&Q!)7Ak`q0-fB=KzX4WAf2X^4i@PZ~weYVG+< z!XbTC9oslKMN%o8ydzDa-Lm+!1afJB$^{T$Neb*bi}w5YfXnU~Q6JJknwr3~yl4hZ^oRX? zHajl9-p{*bNko#uakiv^`m+x3mLV6xz%{aNJOCXC!~XsQJfC#evjBj^I5n&2x7p9{q=Y5OBnq91J;wZ#M% ze$T62vTCVh#aR3a-^WJOYuwd@EVYdU0QiWAV4_Xn&|c((h2uVBB3y+7bD9Xgr< zMhCXd!31&$us^nXyR(pZHml zbU*W!neMaTjAz*g(P7Ey9dKWUVEMj4aZq7ogUL}Px zNq}`V16hwqjS)^FAK7_w2mt%(QIH7qYkn+=en~UR7ekH#dm!S(V|?~$r*-|${J!o< zJmUf_Ik+y@~1^ak&#aX}LPf}vxoA?XAxm77Gs}>Fv&`z2+30Wel3H~cD%yI|`y@BH) zDN3T(ue@1C0xu*!;n2p%$#%{{+Qa+`Ka8oFoQfg``8vw{wkaM-Ifn5@d{@4%@vp*y^ zrMEDhS;#k7zm!2XI{w+U+9l^Uw^@l@@d9#-J2TDxk^YW52uOFbpO)R;EW4GE&7h13 zAQbQB>EV_V7_u0#S3<;=N366#$coJ=G1gok%0eH^GUPqz6S+Q{~^o1;I+k8_S zr)k#!*FNcGinx7tYs< z8aEm9*P&yF4IOvp=)s!CP)7glcv0N`uAc8dd$y^0krrRR3q^DHY;pD~D4^-H#SN?U zg6LiMjvF&#^r&$qcZ?m1x4Q?8z3t9HWA7R|WY}NtDj7Pyq-5C0dxwq0Z~A}e9YdWW zBnA(=L;fB)XzaacHh37lsD^__j2b*-)L#dW7^;3-qsI=tZP>VBqek97V$`6LAxPXc zXapX&M~96cI;3RST|>u}47zLd;8CMS3>`Fb^c}a{HmGFKh*5W(i4o3DO?FL9_85nr zMvTMs2RXl!>#O{;lZ%tvj~g1Lc3m-*@nK8d3=mU{J9F$fr~@(b1O4{gTVFM`+wn6NKM&w%GJY!YL%N`cs93G1 ziB47{Q z(4-K4t!AO|Hf)6?sh#39eC)AJ$*lo8G74QM2K zy6LlZP0N;Pab2C>Hfw-W&sAQ=>*8_b^M_s+uh;1vn#@Ic3)F9lA5w0skVe^$b^7h; zd+~l9@{qoLy?!CTJtqEVJ!o4I6C2j+k*rlQQ;zj4>X7bS6%*bK`c*uCskmW-J|lg} zQd7%8{+gxY*A05xyu(QQkv@iJ2A)IEp2h@KFZ@`)ju*ZmrhTkGl(qW}MZKi*GdJoz zv#v+|U{br>H^tvJ!jByCrg&|mem0-?rucFrsK511Q*sm1S)25p+;~e|wh8d}cuPFK z3H8UnC01|J=STMf$wvv%i@-hjIRiiE4RN!YOe&*n38GhE|ryf7w z;)g9ZwRZTq1wZ5Q^8|kWiJxlx%*W5a@Usa&-{R*4e$qh4miTG2#4ObM;N>#>+>Dl6+{4$2>1g5;TLAZ({#Ofv7w{csb~xB z7H`tkjr5&ZMbFb7v57X*yYv}--Z0XZfJ<) z4{LZjluvhTXjl`Hc|X1Ja;;HCA=~c+s>p}ry|oS3XPlk4b!`g;AE2PAr2;4O?7UA} z>Z%b1yIN|I6Fxid`<4oBL_zJk76#vjf|)H9oEUi^Z&phM-$l;OyT7G^^RwpWJ=9V` zAp7jR$66{Fn>{~oc}oTDbI#6tv896BP_VA0f^Sf;silGuRwXUp(C}TsxwLR2lFkj+ zwK<;lz2C63O}mCGa>r(G{{U!VCbJD^%?Hc2<#rdeb$&y*^>I}HWy2}0FOO_xHY}on zPNDU4L!xz^CjRT1@Ru>sugSkQZ+!~ynVRUYtZn!%|LnYnQY1_joL_KuI-8^lUMx5w zyp>e9xnW-0b`1yH^b)g|^(q`Cbk4KQKwS2_qH%&}xUsm1LERYrUBj@Fjx_DPhVdo4 z>6;A=zi+pYaWb{EQ$x4X5sa6qrPbnZ4bPRf%fDj-(Bb4+vsk*KW+Wr*yt|>{>(UxR za$ftM=y+LO`;$ob{GFKH@XuIJ{cy>$Gdsi>q-8ghb!aExepSmhtzR4d zxumQ3Y1y|+t_w4rF8fCXl4bY&<7pFe?YuYu<4%3C6U22{?bnSI7N1*9ckFGu3f{mP2puD*F(GBv+U{(s@B|%#|5~HE$?3yK<>bg zoMa=P9d58c<3`yH&whLmrO$3xZFYXrgL`Q6nR*)9beFGGbC6Aru-;(2#>~N41SMq) zo2+{p_U=4B^HoNS5snKXA9_LLHJtI~N7)|Zf%PIlQ2(#yVTxP6>LcnKa=xC#v%ll( z4$wuJZw4HxZ> zqU7e?!%KC07vW)BI95#=K&>@$!p@G5mwmPS455ZSYT4NDin;H5f9QdJp8I}q^1kN} zw^Gyl4ax7dt_Ef+<|0HNZg8~Btu+}q&`E8LqeJ2 zRNA*)&I*gD9G59dxhPZgcJq4uAJeED77=~W*VKsUiezy_3^=5#4)TO7F}CQbpID{z z6ow;eVgcviL33Qp#451(C;3K}=$b!s78HuL35%Rss+=`pXXY%Kku54|@okA?v&BUL zYSET`c~rZYoMeJHC;p+M>O< zCo#qrgUy)7QFbWFiW$7lW!OHaZ;c(Y?n%7ULHr5)D^KqzipBm!O-J!Zg?NS{@oOjX zsENWyx{9S?P15j}u!dy&aiWc^{fauvdELZ&pnT)1PKgJ)i=`y)mXj+*VH;hW2D0FX zcuZCR)_(a!rD!Yf?J3$9-px`!IO+>?QJ!0+5LJGu6qRyM1*ln6DK7snI_!JIz7ptb zgfGj(gL6PwKg)~z2&gOhr#|8xSLPfMHG!d@&X26hI0PNu^F^yJj zm*v%B5^(%*HOPTnVzRhYwRpuTw>V+&oO2kHK;O zJDn8Bznm(Dc=zUM;+P~eUwWEi<|(KBe_-ZghKn)`^1I;zg380^OF3NhJ%pp}MA;bv z+H^-^*qN|X^Xj%LBO(Ibr7RrNsoN?qJ4f72TfdTf&JhWuiO0_s2|wMsQ(keg_#A0s z=p|xezMbR%SXBmN5vfmWcwPbaCq5Z3b_iPXP2!CSVxh?*FlM4SH?_F=K#;d4ilb9G zP>V8qk~kOU+sH}c22n3RoCF!Im$`p}MyO9r|C2abv~ktEA3%ql!ZF};{I>3qO*e^I znEQ>B#U)5KO%@YVjIqW<*B9I@VyVIMkaPRZ;&&NpBDM&hA$z?|c?p^#N_`cxn0t$C z@U>G|9R3``TFA?4$z39LMK%kQU)^YD1eZDJ_Yy_|i!=mIRh za66Q*hs7-0Z)Ynk1~e9T*NOX3clr!4e|_Sq8Dg~vX^clqY2OLGFzZfnA`txEonjEA zqU}tvNu&@xhY`L1F0m6d{OoS=LI;gqA9QEbX9uH(9k7Ev#l7axEQ-KhXHJo;?-6rQ z|B`xf9Fhm?1&n&RvR+&jQ?0d0@gpl(W-`rej*sz&#!psLp}csO_zi6?x)<7Uf8wTl z#m8B#HG|qBohQdG6b(Mstn{d8BvgIkF>#KcNk>nz63jjTl5?wmf0CI1BCW-whHo(;%FG6vd#BUH1{RW`(6YD zKVSNTn36Z!GeCtCV#`!GGvAQk{UFZEd$6UFi}ymw+%Iq1EBdBv*Yu{V0#qQk?G+)q z{}y>_7FFJN83pqmYpJiJeuVOn@`4}53ow^u+n>Z}2J5z;#I(GXsRls})-!L&(x1h7 zl`pnXqL>`QWp@E%z_*GNVwtc*Al95)}*8+;}>qvw0 z*0)q}6IJ{dLyXADO&`|<&~JquAn-Rq=jA<>>O94yqlhZn?_7hD_=v z=E@f{sRRmbYbKQ@IpmCRl08SrWnt=+#JXZ!kMr)0P+RdJOk^rX4{IXykG#E}cb*Qi z%%byp?`ok$AuF9cnM|Gn@a|@_#=4(L)HkoGg@$r$HiN5VB@m&o1M&GSn=Z)vsHMuI za}@i^xjAI_(&`^po>@-w{A{4$W})9*4Rn5^m%kYK^6MNLibbT4MHeEex2SX5^iDQ= zmA7)`4VEW{52h>2BuC15xzt(AldE&-Q6}T)R&*`1&(>BnAr%J_S`i87gE!<$tUdnJ znw-3yDd}-_DT>#(Zq8k)l74xJ%3+AM`E)rCF*%?9m_CGxS_x8-bbngyqhO) zDUkOm2jN%C=OH0l}Yh}B3&4Epo)U_i=%$IMpLl@r2H zBZh36$1iovFH76g(Ruf#aG63q$k#zWRZO|EyG=9F0WXMxkZFLsYtU0`8V$ElkD*0m!_*#&}-!vcaSElf{$yei)S+04_W-c$yb-qY;3(8*pCuE#h zMr_56EThh8IN9ZM1#u5t1;$B*+)zfru6L#8kVMR3N+U~&KP=q~yScUrwh6%otmfU znBP?kwM+T0jfkiGa!Y@*x45SEUFqXp&m*;ktw;xT*n$M;z)uJ3s zi3Z)RMT%6oEpr(+aYrAj5GBvKn-l?Kv&~;WdRU$dQ8pgqXd0{nG zA(^G0;G3O*r|+sMUps8rMSUg=AY z+vPA1cBiB!hOiQYV=Hhx=`-N2yO46Z*T3PECmVEE4IYwXkD)e6^f^Z-%loSGhvj^f zd!lYQ{cGU63MXC@6It+E!_S@Nhm$>paP^3HU}-R%`pnTWQ^#2`qgr2$jm61o&`6|# zdhWG8EjuT%MdC~&tX!-gtS{KV)y!DSncUvFtVtvlGD`(?w-US|+_(!(BRJJ2kOBH| z3*sARbE=Jv7=n!;0YtO}6%cPYVA|#&?xR*z(_T|X^F`|sfZ)`$j-7QzO_W?%0L4DG zc44#M?XrVeVPFAkE9C3{&Nit4)M z#JAkbst~4oP>ka`BJ3DZKS%0pWFia$Z4Ca23WZk6LTD!HcSJOR@zVV6xIiOHMn?mK zIeZiKhY^Kf(r7WoV2q$OA*ArcR&xroWfjsS5!W+yJOV1@o2x=VK0O%?M%WAM!eE0K zAc`*<^uUjMSOwPYU^T7dzCjZ;2F6rlU`(vSrn8a}R0ZULLLkme&YKNFA#Y^>?M!Dy zK&{B?9w@D%705?)%4HYpF0PH7#$b)(8;_ef%B;UI*)&9hv(PDHSPKS2E}k4@--dZi zi~1r!45>$<9#IPHS#NQ#ybEgwkEf-W8)jfO25sMCv5=l0(}$lQd z&A843B$&i(jID}}r+RgJyq;sN=x5B~$)ZjjQpKKDwL*l0X`%$OVBM;@j|uMmv%!hr zp@0$x#sicSYNHKqx|l|F++=icf$D|}LaKoC0WpYJZPtb~VQ2gFI)z0zFq~=3Yj4zV z4rDCUR2YWyM@YZZW>{9h5w@7%G}PAm#tqI2fSZUn2M7|uXo3kdTY2Vh$Af^U!nifm z&tO16KMlz25Gxs8J^@8fV`}JFv4If_9L@wpOjgKPD4NMYObbQPYdO`0q7e8tQAS(@{Wx*wdsZ^G1sx07bOJ+*EwzIXv-o+mZ50Fc#z`nL zPUc$yn)AjM^aCIZ+gU8yS?$1yQ0zzHn^ zTiG#AS1QC(5`is^W{M>jiiNi<(-e!qL_l+Q0}z%zR6!I`5vV8!F;2%EQq1}#&QW7b zT`8E&5C~Qfv_q750098eLOBdnB&vmpb5TjT6fsru*s3w|^lqb-(Cd#foGkYBOZE(NRR}(EnIvZ(Qq`bEI#z%{A zyBt}?%rfpMndQ_srna%vbZtCa{439TOzSZRdXx31vr}8CtV3B}vTk+m)B186>sbsF zXCODr1!JLAbg^v2U7);X+K>s_k@H(IV&BE|guY%}?_ zVnobp6KnnwFh8f+RkThi{(eT2N~>r)QndiUJ~21DnQPKBSGk!>z06exmLtvt{OptP zS~AEFz3!v8{*ie0zhYVcVJr(_Ok>${t-#_on?|8% zJD2>2@GC{zVU8ZZLbO3HR&3FBtXjbZ(D3b`zWmVubiQUCauzAAiiVBqM)Qs4f@GbA z@fvUvVf-{EaybU-@NkY1=#S-tpgh4aSUx}mHp3Ldv~wB8`X3o4&StLpzi`a|UxtYv z&M}z$%w{~ISj4%Lc{f_Bc{tkMO>H+ns=2JM;$v2Ttl&Ha2%V-4M$U5;pjyCT|A8H0 zRG~l*TLoFbU6ZmAYCR>$vd|O+>Oo;REv@ZVoU=0+1EtDoSI~OG)w~;ES9+i(WtG~mt;r;l2sB_LdJr| z|5&(e*qxJWvPl^5uMrgDb-?j!dm&5kq8cuTwf6Uqo{K*bVB^J1qRf@QmQbdaWwnAe z$Jhoe*Q{I_J1LB%V`^nU>ycG~B&Hg*$r&$7pKOtuY$K>q4&yc&bFtN4_iVRKN7kMr zB5RL_tWs{j0zQqv+OIrgR`6lx*AzbDwK#3*PMaW?U<+2El{4ZrtBAw zJ)EgpXu>d3_d!{8KniQJjCsTd_an1B4ol90?#sr&tahI5SaU%=WSU8O=dXhV8G>I@je>7`mflrv)4s z5p34_hah}uL>o!cE|%*aroy;& zn&xq3u+Q}I4qArp(aym-rV#}XB6~FO9T^=X<;y4z&IY5@d%dm<@buwk2cn4Z!fLKT zKZqlwEpw<3uZHoK?3D)VwUPybU;rn(LpR!*g*0TLontGXhbz^@u+Rhdz`}5B`vYK0 zHa>#rK)`O7^ezK-N%B6zF1m+N&E-5Dh0_0FIxH8S0xpLS4Y<^da@at@%;PHp(T^ebKgFp^4h1|G^@&{x%kqNrk)WvRTg<+z{@#vt{1c<{P zW1Ox9yP2(iB01nu3db{?_NVZOEJztn!AWpV{7!8JQs_oNl;POyF=sfH zZqar_m+6@}O&g(56^jy_E0eW~nqpc3dtGXD=vM>1x;h%*1ct=33^s173O^SxDwNZD_>3WT91&3=sY6i2 zbYM)t&CCH}7-Pbw(RM~nJjz}|6aoZ{z-k^H>&)RR0|IA(fI1T`QK#Gy(JmMwO(!Ct zWp_@xnOy*B=V%v>UUf;zO6O=fhVz=+;9T5y4%=M-QfJQt?Fv5w9!Sk-1j(R;XrLkx z#R7H+W83gu6lVY$bPkbck|Eg1JkIh9c2=IY4$+azsTjQASJUr*EU_2_bMOG@?1-^JV#>`MEy7hptLGu7afP}!01>vg3nyG6T)?FxkvyPLaU^+3NJ*$T@_`p~Rd zt%TO9b{Py-S6Qyc%52zi=$SPEuX2Iirl(;XYw()Pu5zd*))TkrLE8y2cz%Xm0OZ+i z@T-DjGpNgPAw+G=4H=G)RhQ%IXY9*%CTy?e>_uQHC<4|`Im!Y$TNVzjF zKFBLG_r)tSr?p5Kz&-rDFGCq=h!wubTLf~GVaIX8j{5_ta=_`GSQDTc7E2uA1=nFs8w&j_oGYQjF;WSyO%PSw zPUbO{1BHi){E#~h29JRYCu`Y3>c|!c3a_h)g+OnZATUJBShkWt#|o0@S-Qq8J}@+LM8B3Bw*!6i{2(pf7_Sw-k*yS{SO~Ro9Oc zB(}f+DAIZu%%@9H?_A?}75s8-3!N(KH<-ES?_v}ZnG6k>;5Z4%dE>&QX0 z%9^c3*bZT4N&N|=T07kWG}3W!*ZE;0BT9{R#nZN|W8*c}Vs=2%R6inB+9v@6qr*Wt z)h3Y4y8#IIMGKUp2R2quF zU6_mV2;FXZNr1myZSG_5l$|jh98R6zaQ3JH;xUd2XxG~t!P&0IW`bx()hpp%=b+a; z7@O}Ku^F+Q;b3!L`M>EtYNPHZ6D8VR^}2Ct6|6D%C2=p0yW4}V8v}Merj$ewqQ)BW z?m7o-o3%mE67-$v1@7t(3)}&ya_nDfsXQ!j7f^w_7!5MxOax{}0LfuW&-E-qrc35C zphD~_x(*3Isl_(k2o!EL;19XLtAc%?$z>?mi2}TzU^_|{bIEd)guN06aa4@JCPLZ>rShdGsCyhLW+vQf z4j9)d_ZlQaFHPLbF0{+^df}1hpo9;ns!eBqs-!1&nvON@<9;N>Sd%%;_<*B~H~=<| zJ47>arZNvWXSL@vVS5G10kawF?lr4vD~Fq4y6jYG6>UbU>=Ta0ZNP)w3V}RMTO&Nr zDYipaBjqK16;f51zUTpAblRe!S;al zlxhj4-0nt|n@np7=aFLNt?EpWr>a*K)8&ogILa;Zys1s!Z*ws5KnhD_CE_ z@G*<&hQ&7*gmDciEP5mn<#aXI8=80UWKJn&|w3OIkZmp{kL=iEhDFtM}JY&l-&)m-G&)M!{N(jZ7; zVX0RM#{>|^Os|uMtCgu**eLY|SmWjD0X7_=@uue0wtCICH_zXBp;xfWE8xlPQ&Yq{ zW%_s&a`t-laFoqGiiBL7E0?98Z7Xr9VB?_L)Em_)rU6qa|18}`!#vQfbE4Z5v$@)> zfdL5hz~-B>X0n|;Sbdy>t#`{Dh&yfcE0?5U$*A@X*Pa~AVat)NLKO2En1+o*i^8z# zu_wv)w7)^jD~R(^m_jxol@DYoHI824G_cEzUR?-;+AicMMw&tuuuCcFFar@B?PGn- zt{B_8EYPelvF*kkY2Xo##-2O;pzLViC>V;SgHr{ySO{I41v(dfKETq*6T`*UXaK4! z#%otY?S1VVw%_3w0Xcjb#qwbV<2|Js>`@6jRP$ui=VV4jL$A}?##`lKw+Ej;8g(O`aLX*eG;2}GaQwK zzC+J!$Yb+REsfa!Oj@YwC`dMkvVkq;JLY5#&fqr5%*aTd86+6Qh_-r#9v6n$6A$a~ z;eQwtfjZqY$LtJVesHLgw~u{*P-$40&LA%P&8Y>^Js{`SfNhTX*qD|&n^T`zQ^qaz zcPdhhPU`h+*<}Xx4zJ)qsI`OFLV4gdyb!iDQMm?hbBd+%kFWDX? zm%MzFY`<2FLFvr3Vi-<&ZeEL5QTNNA){3KBdHTXxDBOYNlDDbg_`d`Nc?Sq9N5^~` zC>z#B6%Ic51~H6$R*&UEF&r@7Ke?*NweN`I2S2TB51&5D!u6H&LW5v#gEF?Si)mMz z|s^PZ~4#Q#yJB_L&Pv0a8+lO`6%3oI|3)rg!QA5>hlp1E8-IhaIHgCWM)R$Py z2$oQ1&Ram06LdG>{E^JCGo1lZuHyn#7|?}-vocZt(ZjQzOV7#*Dett4VGR1=gXy2W z`sCciUgJ!a8%OgeRNV}(#~`vkPS1jOdWRmKw;TNBhT(kw#`Mn+qa_{0U9M`on}!H- zdlN)QJDV2;bdiIjJhc;1{7pQ)ycqHm2s_h~bA3^jx`-I8g37NRp0zPO3x?|q7g> zyVXGNedCiyf!Qo2mn|8(>74FR0@aM85+5IEUxDg#kDnCi|MV&`?vD~3yT+4zyDD8O=X zzMYiNN{F98gg|cjNR;MrSOXe^rP2p>RD9oXS#Bq}X$Doxo8J~a`Gv}--WL2u^V@HW zGJf2rA4}d5SGVqAKqPQzS?M37{7L~I+YV?^OU&v=p*DOr`@u7*rzw^t){dZAex#RN zNXv`lo*Sqh!pZBi_n{at_R)j0;B4az5;}Y0Mk*`zF{ z#U$*D?U%c14;%N{NmQ2lSO7+x$cqKi$(YG^lc-HJh2N_+8!_ved|3Zkg(P1KxPT+5 zV3!yF=?J*Ma;#~%^tQubdg*r6NZdqiTdOfRv<5Q?u^?XlzCV@tZR-46s5g?Kw-B#H zf4BvNO2|8Ip`s!WTIhD+Q8uUne&H4>@8c1MBO%Ir0#$G%h-)N$UHC58HB*fgvHafj z`rS(Hv;NL`Xtl?vvHmWvy7drzHnZmCl3S@wRdUSjpa=Jh)=dh!)R=6=vvTu{jfmzD z%k(=pA(>q<#=E!CfC3L05SDb_IJFM@ zyeHmH1kD3KfotY*3l$+LKH3Z3FtJ>cfRC7yx*2sk`iT2mQU(<^`(}78HYwv)C){Vs2j`f4zf>`)ybGSVC+% zGv+HsfSWUH6P7ARf#CDneDstv)BRr?AGSE_PU_lz50_*Ss`+5SVCzveHyjDm$gp0J z58g>VTd#Z%<}e%Z2A{JkfekcAZo3o8_oe%1(lG<{j2s**2LrM1v5mh<=9s)e1n0A# zfrfCJte0K7d-f5Od`{TB(@h(Wd+qhrGLGQ=Rx!)d4pDKPdW!LVLaLo;mKITIlW5j7nJoXW7_K=w06&_H?I zUDT$e$*p8WecToH#8g+f*)?BYcNZOf4t7hNO^BAWH;&bPGXulN??>=ie#9yG{`?qT z++^n-@2kSC0Ew`tiR~YKS`23wcrSPt_l!E&X3LYm-bIB2*BCV@fUhH1AA7fby*tjR zgd;2*o9$7I1OTcCursXzl)Uh6>V5@+!0b1_?*V4vGf#jiFNVDjZh1K7wI6~2P8Vk{r`Btl5DY4xl~G*y)hxWGxqZIucQ3V%Gx$wf z{y|fge^A^}Q5TQ>AP0IEQDE5zC+dm5t_1fDO?rqLc2HbV(~JQ`#j+s{k8%yBvG-5I znx*aMI4cnVe{oc?@A(`8XEAPMJ*8BPwU#@8=F$IHIcjB@o2D#LIT`{Jg$BjXRN5{y zNKB69YE6nlZ0=>)p)+HY)Sww0A>?2-tI%9eh3YZziU~%kWf0C}bzEW0fK8L5-ob@I4Q*6QRNo#d>b8Tg9Y z11s-4ATJ>>>vX;N9E+mEi0%K^9tS&hU_7vl@wh-!`$jJE$l+Tj!u*@gcs6q z%2j=Y4)1N??3(72h1@CPy2b20>2iLNFB;;Y7?`0PD@}S$KN>FI{_=6P23H$I)1o0Z zWt7*FV?za4^#~1M9}TM76zpgV4mo$bgUH3!~%jzLwVMUZLq{@ z*farWD=g4x+vF3M*m}peyL2A5i>-Cc_39LuO9bE@irjRyIfzu&M^?>STWwlx8 zqFSMo5A>?Qx+ehD(Hy3snkt|};8QE@|0E1@-byMQ{lBq(uo>$IHS7PkoG*LYZ!57Cq!gdl14`Yp%!6`;>NRZK<1Nb6 zX&WwcKP%uy-$(Li6u{a%i|s~@HX>pBv!I)W%>=#v|1Rj#d6C)|EaiD%ZRaVoo`>aO zSRcw8U!;q?&yQkvR(|**osh4r=WL%rB7+wnJ~+5-zU;kiEF@)ho0MaA*$3Tz=b?BW+YcSKem)O>WHyy97xc<0?V{*w!F+i zSww+3w(RvXM_D+&$!!%yz)?xk#jyGeL3EA@J2a|>02+?GN&!G#PSpaCg$0JkVdcz# zoaPxOkfk7&owiGz`QULta`cRWx6Q&F$Fj12r&xS1|Kp6&o!4OrV4hR zc@pS_@71d5I(~2iupqMAqG|xIsPONFfl+uIcgK}+tkk>pw7pvPTa*wqFcmw!`cJBA zvywmVsCJ(iW9+5jWXbA}W$V>+3^i?&!{0@4=IYfnJe@~hT1~y$?EF%-#9Of3(gvqs zaoY2x%y(*fd z%5}sBp;|TV)Lt3y*D8-qqeEGlEZcTcrf~+3`<0ciQehjHPr#IEv#$SIUivC^plMs> zv{z|Rw2_w`Mh!d~MlXMILLG&}ky$OfJ6@%-)^)lXj>!$eCr#K^en6JK2JFq*DKB`9 zii4jp8(E*qn_k28mH0NFzLw~;>4#Fs`P|vNRd|+)Z(N4qeKgZmm50#!P=?TWnHfp( zN!{c`O7L7`LQ~wGGu#c+n==&tp|b^z zp@R;U0#_^Xq+M0R>%3wH*?Jva>M=8irYEnXtKAv#O9Q+THn%W3$o6m2m1#{9cfE;E z-;``%x<+BxAh4lV4qJpf)2 z!PW-WH%{F)dDljIi)L+;m%L3~>Ar1p#@n>Q1udwNjqfO?0#y z9P+0t{8Khjart&8cq|CL6*w~{k_tbF-Ejz)(#CkRc=G~z*CskK&-L8-&33g-URYzT zk)Lm(SZ2!kkh$;T!-4CRY8d5Vg}16+ewWVf`A=78V%=bS*JK-#ztnOhZE@mzR4qP` zli#E3()vpL^d7?d$j{rH$~Q5_-J2nx7<<`f;PKEHQw}higBa8OTc|+XCEIKvwZ(Sa z7JSg>XL-#QD$SkFPC%CFnX65Y$Ukp^kD#-$MUD4HACbGaP?Zi=JFJ!`>W{!9ii$7e z9{T}Ej}4*^=vEJ2W}@96Abz#?Ynk;Sb)@OLzW zJMyD&sM`eVmoHdkz2^qKBA7<8x%|ELy#L5$l8yn+X=fkPw84nlX_s;!8@}` zU+{5BeTS@dZgZTUXQL7Hos49|_ww71sG@8&LuLCbjLSF(f2EGy@gWDiG@xrX?v?{S zrVDeEHP7do7hA8$zkUpK?A&X22MtPEPvQekF~mk$sc3JSH=w!bnz!t#BuXApFmRYk$XO&PStAl zf`t=s2(-s;xQIf{*zjkPR;-RF!R#`eO1$<6^#XdH_9+#2bVH$p)}}IdG*~Q#!Ulng z`!SGreM${fyIaOSqpMo)Q7DC(2YcGdh>}zPjePJkI=yNW&jKfIaA2K7XZQ{}HnI_> zt^bC(0uTD-s@J(aKvcT$^#!th6TaoQ^c#6n6OCyB&#RlLOYRpQp>PC%cQ0R&zco=o zx5KiNXseZQdgrLMl>Qw%WQ@f&Z=deIZ>Re8e~ z)VoL<6>RzQv414^(+xBb9|`zbzWfDsY_o#pMIF3RFF1trSc9+lR{r({T>{m8@eb-M zX!$mI|4zEDwc8mOAJJL#!e97q`z1bY*7&U)`X$ZhieG+71Mr;a`W2nVpQ^|A);r1h zUsEqG-1s%MZr#Lqu)bmCZ-g^3p0!W@{54e{HFF_Q!R|AI^DA5AtG2gR#tGe4@O ze;S&y^|Rp-dD$+!LaQn=3h@?`uEe)X`IgTztUHJO z7H6VPM>{#=Vm>}T=pjh1|AvY>Z)MOCLE~P426*jArIc(!hha3a{CZlRpF zo1)zO#og4uYn?kdCU<~kC&F)H<9MgiA_%JC&8MsJhL^MGVcBsH#X&y#mpwEc6=cPC zbS@!(`ggRN(_!D!!&@CN55bxtO0~yfvmlP>hMKkqiVaZIfT7 zg3E_@(gB5%iAuN@ZU?N&mp&qMf22Ifi0u3$^(wqgttraMwd(}UTQGL z(4t4=${+Ei*xTg$KVm!fHhJJj>Pwp*kySrY2RvT*6U?65SVdlwD7JLL&B7{JpIX?g&CDLSN-1nR9~%>5sw-oXU^o!I|psh8*M zqvP8jS$v@oW-gR}+egPT^X}e99S*Gw8S`Wvvq9xv|DwWDg%~*cS{O8%Y&iW0m@yLn z@GqEsNay{FrpJ#wdQy30z=MF9LMs_rd`cCulr9>@h+<%n42TWhH|1hHwM+>Q*{ECw zsZ=;euvISHPbar>)rrqDe4$GA@5e%mw9PLx-dgl9x=x1speP>r1;P4@Qix!^;09QMjm87i?hFq>z?G{E5n$HxZfbeQ;krX)iNJ#@IY2HE^ zBC{lCudx zg0_I%Wr`O7Rgl9}Fw^QW5i!CcRA4(y4FO=!44DHe+yy)^q=?(Av&c4oBUIDfqe0qa zk7LXn2$_!_ah~fl%*i8MXc-ECsGwCK8u$=*}^ktt~WH- z`)osV{enz$TyIy3lTxlN0=P>yX*uDu3g9PL4Uv%(k7( zjhH8kee%MHS=v_bL?dDm{yrqa_#=om;)`&3_Q_chGoO9t#)x?qFJuQI=I`47rhE$= zqCrJv!6;%X)F~@rQkGeMj7wnVa(vSzMOjyP?6l@7jIqGT53d?MH}-PX36qGGltw@h2}>3?G{o~;1nj!BgGP~v9!WGo^C*~L74>LPEACc>e%#!%eEK>SomDoSB z4yZ@`sa6c_1{_{BtlRYfHs?;|d-esqSN;&7;6`ERwPZ!D2NfI)Z;g&(h93~Z7q{xT z1D15>IO|?nQw$BbX}+9YY<9w*&iF^Md0gq8T*Feo!)@Ff{fNolo+v0W-^vto6Q6f7 zAEI~zSH>Tp3irV4A>3^esWI>Mb+uVfK&SV;XT&_hK42~!!7t?A&wYf?!P!HN!D;N% zDm3k4(}>^xanv1LRxX0ofp!Ky|2(V@w#OzoOaF{q^{4OV^B=hbJ^4_dRNekgb*mto z!x=F^DEoCaqj2Fb?rP#Mg(N0+HAjl9FX#K9LfHWizcD}YRX6i#(Q4Xa%edY!j1&Ea zaTL;NixW>)m^Zc}b9tiUK=Un=#x73$c!GK3B+eQ8ym?3K#KcYJSwg<^u6a}2aW{+{ zKWgOlJ+8arOx#Tp=z)Je`-{xlXqLw5G0On2kuWzmz zbM565uDte!E5?kx=7zB-`~8(8e?MyMdVJpaqYEZMqYl+gi&Kh zUO(aTvDejHf9*9_Trp|F$jirGd*uydMxHk6`s+ts^9TF-krO5$z21G6XKXaP`3z&> zWAf&WP;ILplaFpR%jD_r;U#RNv{6oa&#VbeYqX5E08%d5V3tv1VcwX|XoUz3$%{&#) z1^W5vEoLlp0-lTY^EQ57h-U@^^)o&&uW3~P%CyFF7o=gN4a?+=56m8!r{Xz^=dp|B zOCOkjqG^leaUYsj6^5R)jBH#*xVqr#jq6xka#f|-S6;f=%#a5@G>@f;Ps(GqLeUgH zB_H2v9-sNZQ}|2A=s!&nPZn)Mh5avUYdC2BeTdHQn$~mS&#d3k%#9#pP9!~_A~N^&&;mW_Zj)nXJ)L`m}e|C)_l}q5g7A~ zeD^aL1+P9MyEK_|GcS48GV+l>{#p5E6aGT;19%SM`5~m)NF!*^V}fv5pPT2>hG*r- z&&}If%}i*5rj`Htx!JFkf%=iOcB>L{=oc{9BP-<8FU+H8#0vS$7hs6m6_#Q!Jb&_q z*^d^lknMH={_QK|B|A{xeoo%E!+a`U55zCT^)#-3;(7Ru%br!Bman<0uVVNc0sxU834EfTWaT+!LJ+ZOMR~C@J_V>+gh4?H~ OB{Si?P_DrrM)`m7ufY@m diff --git a/internal/sqlc/sqlc.go b/internal/sqlc/sqlc.go index 3e412d5f4a..53f645bba2 100644 --- a/internal/sqlc/sqlc.go +++ b/internal/sqlc/sqlc.go @@ -21,8 +21,8 @@ type ConfigContext struct { Dir string Module string Engine string - SchemaPaths string - QueryPaths string + SchemaPaths []string + QueryPaths []string OutDir string Plugin WASMPlugin } @@ -62,7 +62,7 @@ func AddQueriesToSchema(ctx context.Context, projectRoot string, mc moduleconfig return false, fmt.Errorf("failed to create SQLC config: %w", err) } // directories exist but contain no SQL files - if cfg.QueryPaths == "" || cfg.SchemaPaths == "" { + if len(cfg.QueryPaths) == 0 || len(cfg.SchemaPaths) == 0 { return false, nil } @@ -166,10 +166,10 @@ func computeSHA256(path string) (string, error) { return fmt.Sprintf("%x", hash.Sum(nil)), nil } -func findSQLFiles(dir string, relativeToDir string) (string, error) { +func findSQLFiles(dir string, relativeToDir string) ([]string, error) { relDir, err := filepath.Rel(relativeToDir, dir) if err != nil { - return "", fmt.Errorf("failed to get SQL directory relative to %s: %w", relativeToDir, err) + return nil, fmt.Errorf("failed to get SQL directory relative to %s: %w", relativeToDir, err) } var sqlFiles []string err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { @@ -182,9 +182,9 @@ func findSQLFiles(dir string, relativeToDir string) (string, error) { return nil }) if err != nil { - return "", fmt.Errorf("failed to walk SQL files: %w", err) + return nil, fmt.Errorf("failed to walk SQL files: %w", err) } - return strings.Join(sqlFiles, ","), nil + return sqlFiles, nil } func validateSQLConfigs(config moduleconfig.AbsModuleConfig) error { diff --git a/internal/sqlc/sqlc_test.go b/internal/sqlc/sqlc_test.go index fb3d3f749b..80c8df56f6 100644 --- a/internal/sqlc/sqlc_test.go +++ b/internal/sqlc/sqlc_test.go @@ -3,6 +3,7 @@ package sqlc import ( "context" "os" + "path/filepath" "testing" "github.com/alecthomas/assert/v2" @@ -13,10 +14,15 @@ import ( ) func TestAddQueriesToSchema(t *testing.T) { - t.Skip("flaky") - tmpDir := t.TempDir() + if err := os.RemoveAll(filepath.Join(os.TempDir(), ".ftl")); err != nil { + t.Fatal(err) + } + + tmpDir, err := os.MkdirTemp("", "sqlc-test-*") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) - err := scaffolder.Scaffold("testdata", tmpDir, nil) + err = scaffolder.Scaffold("testdata", tmpDir, nil) assert.NoError(t, err) mc := moduleconfig.ModuleConfig{ Dir: tmpDir, @@ -43,7 +49,7 @@ func TestAddQueriesToSchema(t *testing.T) { Name: "test", Decls: []schema.Decl{ &schema.Data{ - Name: "GetRequestDataResult", + Name: "CreateRequestQuery", Fields: []*schema.Field{ { Name: "data", @@ -57,22 +63,8 @@ func TestAddQueriesToSchema(t *testing.T) { }, }, }, - &schema.Verb{ - Name: "getRequestData", - Request: &schema.Unit{}, - Response: &schema.Array{Element: &schema.Ref{ - Module: "test", - Name: "GetRequestDataResult", - }}, - Metadata: []schema.Metadata{ - &schema.MetadataSQLQuery{ - Query: "SELECT data FROM requests", - Command: "many", - }, - }, - }, &schema.Data{ - Name: "CreateRequestQuery", + Name: "GetRequestDataResult", Fields: []*schema.Field{ { Name: "data", @@ -97,6 +89,20 @@ func TestAddQueriesToSchema(t *testing.T) { }, }, }, + &schema.Verb{ + Name: "getRequestData", + Request: &schema.Unit{}, + Response: &schema.Array{Element: &schema.Ref{ + Module: "test", + Name: "GetRequestDataResult", + }}, + Metadata: []schema.Metadata{ + &schema.MetadataSQLQuery{ + Query: "SELECT data FROM requests", + Command: "many", + }, + }, + }, }, } diff --git a/internal/sqlc/template/sqlc.yml.tmpl b/internal/sqlc/template/sqlc.yml.tmpl index 0ad50bcd7a..48bc5e1253 100644 --- a/internal/sqlc/template/sqlc.yml.tmpl +++ b/internal/sqlc/template/sqlc.yml.tmpl @@ -5,8 +5,14 @@ plugins: url: {{ .Plugin.URL }} sha256: {{ .Plugin.SHA256 }} sql: -- schema: {{ .SchemaPaths }} - queries: {{ .QueryPaths }} +- schema: + {{- range .SchemaPaths }} + - {{ . }} + {{- end }} + queries: + {{- range .QueryPaths }} + - {{ . }} + {{- end }} engine: {{ .Engine }} codegen: - out: . diff --git a/internal/sqlc/testdata/db/schema/schema.sql b/internal/sqlc/testdata/db/schema/schema.sql index 3582db502b..5b6627fa62 100644 --- a/internal/sqlc/testdata/db/schema/schema.sql +++ b/internal/sqlc/testdata/db/schema/schema.sql @@ -1,7 +1,7 @@ -- migrate:up CREATE TABLE requests ( - data TEXT, + data TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- migrate:down diff --git a/internal/watch/testdata/alpha/go.mod b/internal/watch/testdata/alpha/go.mod index 4d8c03dc56..47a1c3be5a 100644 --- a/internal/watch/testdata/alpha/go.mod +++ b/internal/watch/testdata/alpha/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/internal/watch/testdata/alpha/go.sum b/internal/watch/testdata/alpha/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/internal/watch/testdata/alpha/go.sum +++ b/internal/watch/testdata/alpha/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.py b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.py new file mode 100644 index 0000000000..33d9468d1a --- /dev/null +++ b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: xyz/block/ftl/query/v1/query.proto +# Protobuf Python Version: 5.29.2 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 2, + '', + 'xyz/block/ftl/query/v1/query.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from xyz.block.ftl.v1 import ftl_pb2 as xyz_dot_block_dot_ftl_dot_v1_dot_ftl__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"xyz/block/ftl/query/v1/query.proto\x12\x16xyz.block.ftl.query.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1axyz/block/ftl/v1/ftl.proto\"\x19\n\x17\x42\x65ginTransactionRequest\"\x84\x01\n\x18\x42\x65ginTransactionResponse\x12%\n\x0etransaction_id\x18\x01 \x01(\tR\rtransactionId\x12\x41\n\x06status\x18\x02 \x01(\x0e\x32).xyz.block.ftl.query.v1.TransactionStatusR\x06status\"A\n\x18\x43ommitTransactionRequest\x12%\n\x0etransaction_id\x18\x01 \x01(\tR\rtransactionId\"^\n\x19\x43ommitTransactionResponse\x12\x41\n\x06status\x18\x01 \x01(\x0e\x32).xyz.block.ftl.query.v1.TransactionStatusR\x06status\"C\n\x1aRollbackTransactionRequest\x12%\n\x0etransaction_id\x18\x01 \x01(\tR\rtransactionId\"`\n\x1bRollbackTransactionResponse\x12\x41\n\x06status\x18\x01 \x01(\x0e\x32).xyz.block.ftl.query.v1.TransactionStatusR\x06status\"\xa6\x02\n\x08SQLValue\x12#\n\x0cstring_value\x18\x01 \x01(\tH\x00R\x0bstringValue\x12\x1d\n\tint_value\x18\x02 \x01(\x03H\x00R\x08intValue\x12!\n\x0b\x66loat_value\x18\x03 \x01(\x01H\x00R\nfloatValue\x12\x1f\n\nbool_value\x18\x04 \x01(\x08H\x00R\tboolValue\x12!\n\x0b\x62ytes_value\x18\x05 \x01(\x0cH\x00R\nbytesValue\x12\x45\n\x0ftimestamp_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\x0etimestampValue\x12\x1f\n\nnull_value\x18\x07 \x01(\x08H\x00R\tnullValueB\x07\n\x05value\"\xd1\x02\n\x13\x45xecuteQueryRequest\x12\x17\n\x07raw_sql\x18\x01 \x01(\tR\x06rawSql\x12\x46\n\x0c\x63ommand_type\x18\x02 \x01(\x0e\x32#.xyz.block.ftl.query.v1.CommandTypeR\x0b\x63ommandType\x12@\n\nparameters\x18\x03 \x03(\x0b\x32 .xyz.block.ftl.query.v1.SQLValueR\nparameters\x12%\n\x0eresult_columns\x18\x06 \x03(\tR\rresultColumns\x12*\n\x0etransaction_id\x18\x04 \x01(\tH\x00R\rtransactionId\x88\x01\x01\x12\"\n\nbatch_size\x18\x05 \x01(\x05H\x01R\tbatchSize\x88\x01\x01\x42\x11\n\x0f_transaction_idB\r\n\x0b_batch_size\"\xae\x01\n\x14\x45xecuteQueryResponse\x12\x45\n\x0b\x65xec_result\x18\x01 \x01(\x0b\x32\".xyz.block.ftl.query.v1.ExecResultH\x00R\nexecResult\x12\x45\n\x0brow_results\x18\x02 \x01(\x0b\x32\".xyz.block.ftl.query.v1.RowResultsH\x00R\nrowResultsB\x08\n\x06result\"o\n\nExecResult\x12#\n\rrows_affected\x18\x01 \x01(\x03R\x0crowsAffected\x12)\n\x0elast_insert_id\x18\x02 \x01(\x03H\x00R\x0clastInsertId\x88\x01\x01\x42\x11\n\x0f_last_insert_id\"\xc4\x01\n\nRowResults\x12@\n\x04rows\x18\x01 \x03(\x0b\x32,.xyz.block.ftl.query.v1.RowResults.RowsEntryR\x04rows\x12\x19\n\x08has_more\x18\x02 \x01(\x08R\x07hasMore\x1aY\n\tRowsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32 .xyz.block.ftl.query.v1.SQLValueR\x05value:\x02\x38\x01*v\n\x11TransactionStatus\x12\"\n\x1eTRANSACTION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n\x1aTRANSACTION_STATUS_SUCCESS\x10\x01\x12\x1d\n\x19TRANSACTION_STATUS_FAILED\x10\x02*o\n\x0b\x43ommandType\x12\x1c\n\x18\x43OMMAND_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11\x43OMMAND_TYPE_EXEC\x10\x01\x12\x14\n\x10\x43OMMAND_TYPE_ONE\x10\x02\x12\x15\n\x11\x43OMMAND_TYPE_MANY\x10\x03\x32\xb8\x04\n\x0cQueryService\x12J\n\x04Ping\x12\x1d.xyz.block.ftl.v1.PingRequest\x1a\x1e.xyz.block.ftl.v1.PingResponse\"\x03\x90\x02\x01\x12u\n\x10\x42\x65ginTransaction\x12/.xyz.block.ftl.query.v1.BeginTransactionRequest\x1a\x30.xyz.block.ftl.query.v1.BeginTransactionResponse\x12x\n\x11\x43ommitTransaction\x12\x30.xyz.block.ftl.query.v1.CommitTransactionRequest\x1a\x31.xyz.block.ftl.query.v1.CommitTransactionResponse\x12~\n\x13RollbackTransaction\x12\x32.xyz.block.ftl.query.v1.RollbackTransactionRequest\x1a\x33.xyz.block.ftl.query.v1.RollbackTransactionResponse\x12k\n\x0c\x45xecuteQuery\x12+.xyz.block.ftl.query.v1.ExecuteQueryRequest\x1a,.xyz.block.ftl.query.v1.ExecuteQueryResponse0\x01\x42\x44ZBgithub.com/block/ftl/backend/protos/xyz/block/ftl/query/v1;querypbb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'xyz.block.ftl.query.v1.query_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZBgithub.com/block/ftl/backend/protos/xyz/block/ftl/query/v1;querypb' + _globals['_ROWRESULTS_ROWSENTRY']._loaded_options = None + _globals['_ROWRESULTS_ROWSENTRY']._serialized_options = b'8\001' + _globals['_QUERYSERVICE'].methods_by_name['Ping']._loaded_options = None + _globals['_QUERYSERVICE'].methods_by_name['Ping']._serialized_options = b'\220\002\001' + _globals['_TRANSACTIONSTATUS']._serialized_start=1741 + _globals['_TRANSACTIONSTATUS']._serialized_end=1859 + _globals['_COMMANDTYPE']._serialized_start=1861 + _globals['_COMMANDTYPE']._serialized_end=1972 + _globals['_BEGINTRANSACTIONREQUEST']._serialized_start=123 + _globals['_BEGINTRANSACTIONREQUEST']._serialized_end=148 + _globals['_BEGINTRANSACTIONRESPONSE']._serialized_start=151 + _globals['_BEGINTRANSACTIONRESPONSE']._serialized_end=283 + _globals['_COMMITTRANSACTIONREQUEST']._serialized_start=285 + _globals['_COMMITTRANSACTIONREQUEST']._serialized_end=350 + _globals['_COMMITTRANSACTIONRESPONSE']._serialized_start=352 + _globals['_COMMITTRANSACTIONRESPONSE']._serialized_end=446 + _globals['_ROLLBACKTRANSACTIONREQUEST']._serialized_start=448 + _globals['_ROLLBACKTRANSACTIONREQUEST']._serialized_end=515 + _globals['_ROLLBACKTRANSACTIONRESPONSE']._serialized_start=517 + _globals['_ROLLBACKTRANSACTIONRESPONSE']._serialized_end=613 + _globals['_SQLVALUE']._serialized_start=616 + _globals['_SQLVALUE']._serialized_end=910 + _globals['_EXECUTEQUERYREQUEST']._serialized_start=913 + _globals['_EXECUTEQUERYREQUEST']._serialized_end=1250 + _globals['_EXECUTEQUERYRESPONSE']._serialized_start=1253 + _globals['_EXECUTEQUERYRESPONSE']._serialized_end=1427 + _globals['_EXECRESULT']._serialized_start=1429 + _globals['_EXECRESULT']._serialized_end=1540 + _globals['_ROWRESULTS']._serialized_start=1543 + _globals['_ROWRESULTS']._serialized_end=1739 + _globals['_ROWRESULTS_ROWSENTRY']._serialized_start=1650 + _globals['_ROWRESULTS_ROWSENTRY']._serialized_end=1739 + _globals['_QUERYSERVICE']._serialized_start=1975 + _globals['_QUERYSERVICE']._serialized_end=2543 +# @@protoc_insertion_point(module_scope) diff --git a/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.pyi b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.pyi new file mode 100644 index 0000000000..ad1181a426 --- /dev/null +++ b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/query/v1/query_pb2.pyi @@ -0,0 +1,130 @@ +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from xyz.block.ftl.v1 import ftl_pb2 as _ftl_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class TransactionStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + TRANSACTION_STATUS_UNSPECIFIED: _ClassVar[TransactionStatus] + TRANSACTION_STATUS_SUCCESS: _ClassVar[TransactionStatus] + TRANSACTION_STATUS_FAILED: _ClassVar[TransactionStatus] + +class CommandType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + COMMAND_TYPE_UNSPECIFIED: _ClassVar[CommandType] + COMMAND_TYPE_EXEC: _ClassVar[CommandType] + COMMAND_TYPE_ONE: _ClassVar[CommandType] + COMMAND_TYPE_MANY: _ClassVar[CommandType] +TRANSACTION_STATUS_UNSPECIFIED: TransactionStatus +TRANSACTION_STATUS_SUCCESS: TransactionStatus +TRANSACTION_STATUS_FAILED: TransactionStatus +COMMAND_TYPE_UNSPECIFIED: CommandType +COMMAND_TYPE_EXEC: CommandType +COMMAND_TYPE_ONE: CommandType +COMMAND_TYPE_MANY: CommandType + +class BeginTransactionRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class BeginTransactionResponse(_message.Message): + __slots__ = ("transaction_id", "status") + TRANSACTION_ID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + transaction_id: str + status: TransactionStatus + def __init__(self, transaction_id: _Optional[str] = ..., status: _Optional[_Union[TransactionStatus, str]] = ...) -> None: ... + +class CommitTransactionRequest(_message.Message): + __slots__ = ("transaction_id",) + TRANSACTION_ID_FIELD_NUMBER: _ClassVar[int] + transaction_id: str + def __init__(self, transaction_id: _Optional[str] = ...) -> None: ... + +class CommitTransactionResponse(_message.Message): + __slots__ = ("status",) + STATUS_FIELD_NUMBER: _ClassVar[int] + status: TransactionStatus + def __init__(self, status: _Optional[_Union[TransactionStatus, str]] = ...) -> None: ... + +class RollbackTransactionRequest(_message.Message): + __slots__ = ("transaction_id",) + TRANSACTION_ID_FIELD_NUMBER: _ClassVar[int] + transaction_id: str + def __init__(self, transaction_id: _Optional[str] = ...) -> None: ... + +class RollbackTransactionResponse(_message.Message): + __slots__ = ("status",) + STATUS_FIELD_NUMBER: _ClassVar[int] + status: TransactionStatus + def __init__(self, status: _Optional[_Union[TransactionStatus, str]] = ...) -> None: ... + +class SQLValue(_message.Message): + __slots__ = ("string_value", "int_value", "float_value", "bool_value", "bytes_value", "timestamp_value", "null_value") + STRING_VALUE_FIELD_NUMBER: _ClassVar[int] + INT_VALUE_FIELD_NUMBER: _ClassVar[int] + FLOAT_VALUE_FIELD_NUMBER: _ClassVar[int] + BOOL_VALUE_FIELD_NUMBER: _ClassVar[int] + BYTES_VALUE_FIELD_NUMBER: _ClassVar[int] + TIMESTAMP_VALUE_FIELD_NUMBER: _ClassVar[int] + NULL_VALUE_FIELD_NUMBER: _ClassVar[int] + string_value: str + int_value: int + float_value: float + bool_value: bool + bytes_value: bytes + timestamp_value: _timestamp_pb2.Timestamp + null_value: bool + def __init__(self, string_value: _Optional[str] = ..., int_value: _Optional[int] = ..., float_value: _Optional[float] = ..., bool_value: bool = ..., bytes_value: _Optional[bytes] = ..., timestamp_value: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., null_value: bool = ...) -> None: ... + +class ExecuteQueryRequest(_message.Message): + __slots__ = ("raw_sql", "command_type", "parameters", "result_columns", "transaction_id", "batch_size") + RAW_SQL_FIELD_NUMBER: _ClassVar[int] + COMMAND_TYPE_FIELD_NUMBER: _ClassVar[int] + PARAMETERS_FIELD_NUMBER: _ClassVar[int] + RESULT_COLUMNS_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_ID_FIELD_NUMBER: _ClassVar[int] + BATCH_SIZE_FIELD_NUMBER: _ClassVar[int] + raw_sql: str + command_type: CommandType + parameters: _containers.RepeatedCompositeFieldContainer[SQLValue] + result_columns: _containers.RepeatedScalarFieldContainer[str] + transaction_id: str + batch_size: int + def __init__(self, raw_sql: _Optional[str] = ..., command_type: _Optional[_Union[CommandType, str]] = ..., parameters: _Optional[_Iterable[_Union[SQLValue, _Mapping]]] = ..., result_columns: _Optional[_Iterable[str]] = ..., transaction_id: _Optional[str] = ..., batch_size: _Optional[int] = ...) -> None: ... + +class ExecuteQueryResponse(_message.Message): + __slots__ = ("exec_result", "row_results") + EXEC_RESULT_FIELD_NUMBER: _ClassVar[int] + ROW_RESULTS_FIELD_NUMBER: _ClassVar[int] + exec_result: ExecResult + row_results: RowResults + def __init__(self, exec_result: _Optional[_Union[ExecResult, _Mapping]] = ..., row_results: _Optional[_Union[RowResults, _Mapping]] = ...) -> None: ... + +class ExecResult(_message.Message): + __slots__ = ("rows_affected", "last_insert_id") + ROWS_AFFECTED_FIELD_NUMBER: _ClassVar[int] + LAST_INSERT_ID_FIELD_NUMBER: _ClassVar[int] + rows_affected: int + last_insert_id: int + def __init__(self, rows_affected: _Optional[int] = ..., last_insert_id: _Optional[int] = ...) -> None: ... + +class RowResults(_message.Message): + __slots__ = ("rows", "has_more") + class RowsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SQLValue + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SQLValue, _Mapping]] = ...) -> None: ... + ROWS_FIELD_NUMBER: _ClassVar[int] + HAS_MORE_FIELD_NUMBER: _ClassVar[int] + rows: _containers.MessageMap[str, SQLValue] + has_more: bool + def __init__(self, rows: _Optional[_Mapping[str, SQLValue]] = ..., has_more: bool = ...) -> None: ... diff --git a/smoketest/origin/go.mod b/smoketest/origin/go.mod index 9009a7f04e..b9c1340c17 100644 --- a/smoketest/origin/go.mod +++ b/smoketest/origin/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/smoketest/origin/go.sum b/smoketest/origin/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/smoketest/origin/go.sum +++ b/smoketest/origin/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/smoketest/relay/go.mod b/smoketest/relay/go.mod index 93c0f96391..b6a0eba1fa 100644 --- a/smoketest/relay/go.mod +++ b/smoketest/relay/go.mod @@ -34,6 +34,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect diff --git a/smoketest/relay/go.sum b/smoketest/relay/go.sum index 36d278309f..ea61b3ba4c 100644 --- a/smoketest/relay/go.sum +++ b/smoketest/relay/go.sum @@ -159,10 +159,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/sqlc-gen-ftl/src/plugin/mod.rs b/sqlc-gen-ftl/src/plugin/mod.rs index d8fd466284..e34ea9ec36 100644 --- a/sqlc-gen-ftl/src/plugin/mod.rs +++ b/sqlc-gen-ftl/src/plugin/mod.rs @@ -34,11 +34,11 @@ fn generate_schema(request: &pluginpb::GenerateRequest) -> Result schemapb::Decl { } } -fn to_verb_request(query: &pluginpb::Query) -> schemapb::Decl { +fn to_verb_request(query: &pluginpb::Query, req: &pluginpb::GenerateRequest) -> schemapb::Decl { let upper_camel_name = to_upper_camel(&query.name); schemapb::Decl { value: Some(schemapb::decl::Value::Data(schemapb::Data { @@ -112,7 +112,7 @@ fn to_verb_request(query: &pluginpb::Query) -> schemapb::Decl { }; let sql_type = param.column.as_ref().and_then(|col| col.r#type.as_ref()); - to_schema_field(name, param.column.as_ref(), sql_type) + to_schema_field(name, param.column.as_ref(), sql_type, req) }).collect(), pos: None, comments: Vec::new(), @@ -121,7 +121,7 @@ fn to_verb_request(query: &pluginpb::Query) -> schemapb::Decl { } } -fn to_verb_response(query: &pluginpb::Query) -> schemapb::Decl { +fn to_verb_response(query: &pluginpb::Query, req: &pluginpb::GenerateRequest) -> schemapb::Decl { let pascal_name = to_upper_camel(&query.name); schemapb::Decl { value: Some(schemapb::decl::Value::Data(schemapb::Data { @@ -129,7 +129,7 @@ fn to_verb_response(query: &pluginpb::Query) -> schemapb::Decl { export: false, type_parameters: Vec::new(), fields: query.columns.iter().map(|col| { - to_schema_field(col.name.clone(), Some(col), col.r#type.as_ref()) + to_schema_field(col.name.clone(), Some(col), col.r#type.as_ref(), req) }).collect(), pos: None, comments: Vec::new(), @@ -138,7 +138,7 @@ fn to_verb_response(query: &pluginpb::Query) -> schemapb::Decl { } } -fn to_schema_field(name: String, col: Option<&pluginpb::Column>, sql_type: Option<&pluginpb::Identifier>) -> schemapb::Field { +fn to_schema_field(name: String, col: Option<&pluginpb::Column>, sql_type: Option<&pluginpb::Identifier>, req: &pluginpb::GenerateRequest) -> schemapb::Field { let mut metadata = Vec::new(); if let Some(col) = col { @@ -154,15 +154,14 @@ fn to_schema_field(name: String, col: Option<&pluginpb::Column>, sql_type: Optio } } - // No associated data column schemapb::Field { - name, - r#type: Some(sql_type.map_or_else( - || schemapb::Type { + name: name.to_case(Case::Camel), + r#type: Some(match (col, sql_type) { + (Some(col), _) => to_schema_type(req, col), + (None, _) => schemapb::Type { value: Some(schemapb::r#type::Value::Any(schemapb::Any { pos: None })), }, - to_schema_type - )), + }), pos: None, comments: Vec::new(), metadata, @@ -188,31 +187,6 @@ fn to_schema_unit() -> schemapb::Type { } } -fn to_schema_type(sql_type: &pluginpb::Identifier) -> schemapb::Type { - let value = match sql_type.name.as_str() { - "integer" | "bigint" | "smallint" | "serial" | "bigserial" => - TypeValue::Int(schemapb::Int { pos: None }), - "real" | "float" | "double" | "numeric" | "decimal" => - TypeValue::Float(schemapb::Float { pos: None }), - "text" | "varchar" | "char" | "uuid" => - TypeValue::String(schemapb::String { pos: None }), - "boolean" => - TypeValue::Bool(schemapb::Bool { pos: None }), - "timestamp" | "date" | "time" => - TypeValue::Time(schemapb::Time { pos: None }), - "json" | "jsonb" => - TypeValue::Any(schemapb::Any { pos: None }), - "bytea" | "blob" => - TypeValue::Bytes(schemapb::Bytes { pos: None }), - _ => - TypeValue::Any(schemapb::Any { pos: None }), - }; - - schemapb::Type { - value: Some(value), - } -} - fn get_module_name(req: &pluginpb::GenerateRequest) -> Result { let codegen = req.settings .as_ref() @@ -239,3 +213,298 @@ fn to_upper_camel(s: &str) -> String { .map(|part| part.to_case(Case::Title)) .collect() } + +fn to_schema_type(req: &pluginpb::GenerateRequest, col: &pluginpb::Column) -> schemapb::Type { + let engine = req.settings + .as_ref() + .and_then(|s| Some(s.engine.as_str())) + .unwrap_or("mysql"); + + match engine { + "mysql" => mysql_to_schema_type(col), + "postgresql" | "postgres" => postgresql_to_schema_type(col), + _ => mysql_to_schema_type(col) + } +} + +fn mysql_to_schema_type(col: &pluginpb::Column) -> schemapb::Type { + let column_type = col.r#type.as_ref() + .map(|t| t.name.to_lowercase()) + .unwrap_or_default(); + let not_null = col.not_null || col.is_array; + let length = col.length; + + let value = match column_type.as_str() { + "varchar" | "text" | "char" | "tinytext" | "mediumtext" | "longtext" => { + if not_null { + TypeValue::String(schemapb::String { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::String(schemapb::String { pos: None })) + })) + })) + } + }, + "tinyint" => { + if length == 1 { + if not_null { + TypeValue::Bool(schemapb::Bool { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Bool(schemapb::Bool { pos: None })) + })) + })) + } + } else { + if not_null { + TypeValue::Int(schemapb::Int { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Int(schemapb::Int { pos: None })) + })) + })) + } + } + }, + "year" | "smallint" => { + if not_null { + TypeValue::Int(schemapb::Int { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Int(schemapb::Int { pos: None })) + })) + })) + } + }, + "int" | "integer" | "mediumint" | "bigint" => { + if not_null { + TypeValue::Int(schemapb::Int { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Int(schemapb::Int { pos: None })) + })) + })) + } + }, + "blob" | "binary" | "varbinary" | "tinyblob" | "mediumblob" | "longblob" => { + if not_null { + TypeValue::Bytes(schemapb::Bytes { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Bytes(schemapb::Bytes { pos: None })) + })) + })) + } + }, + "double" | "double precision" | "real" | "float" => { + if not_null { + TypeValue::Float(schemapb::Float { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Float(schemapb::Float { pos: None })) + })) + })) + } + }, + "decimal" | "dec" | "fixed" => { + if not_null { + TypeValue::String(schemapb::String { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::String(schemapb::String { pos: None })) + })) + })) + } + }, + "enum" => TypeValue::String(schemapb::String { pos: None }), + "date" | "timestamp" | "datetime" | "time" => { + if not_null { + TypeValue::Time(schemapb::Time { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Time(schemapb::Time { pos: None })) + })) + })) + } + }, + "boolean" | "bool" => { + if not_null { + TypeValue::Bool(schemapb::Bool { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Bool(schemapb::Bool { pos: None })) + })) + })) + } + }, + "json" => TypeValue::Any(schemapb::Any { pos: None }), + "any" => TypeValue::Any(schemapb::Any { pos: None }), + _ => { + // TODO: Handle enum types from catalog + TypeValue::Any(schemapb::Any { pos: None }) + } + }; + + schemapb::Type { + value: Some(value), + } +} + +fn postgresql_to_schema_type(col: &pluginpb::Column) -> schemapb::Type { + let column_type = col.r#type.as_ref() + .map(|t| t.name.to_lowercase()) + .unwrap_or_default(); + let not_null = col.not_null || col.is_array; + + let value = match column_type.as_str() { + "smallint" | "int2" | "pg_catalog.int2" | + "integer" | "int" | "int4" | "pg_catalog.int4" | + "bigint" | "int8" | "pg_catalog.int8" | + "smallserial" | "serial2" | "pg_catalog.serial2" | + "serial" | "serial4" | "pg_catalog.serial4" | + "bigserial" | "serial8" | "pg_catalog.serial8" => { + if not_null { + TypeValue::Int(schemapb::Int { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Int(schemapb::Int { pos: None })) + })) + })) + } + }, + + "float" | "double precision" | "float8" | "pg_catalog.float8" | + "real" | "float4" | "pg_catalog.float4" => { + if not_null { + TypeValue::Float(schemapb::Float { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Float(schemapb::Float { pos: None })) + })) + })) + } + }, + + "numeric" | "decimal" | "pg_catalog.numeric" | "money" => { + if not_null { + TypeValue::String(schemapb::String { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::String(schemapb::String { pos: None })) + })) + })) + } + }, + + "boolean" | "bool" | "pg_catalog.bool" => { + if not_null { + TypeValue::Bool(schemapb::Bool { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Bool(schemapb::Bool { pos: None })) + })) + })) + } + }, + + "json" | "jsonb" => TypeValue::Any(schemapb::Any { pos: None }), + + "bytea" | "blob" | "pg_catalog.bytea" => TypeValue::Bytes(schemapb::Bytes { pos: None }), + + "date" | "pg_catalog.time" | "pg_catalog.timetz" | + "pg_catalog.timestamp" | "pg_catalog.timestamptz" | "timestamptz" | + "time" | "timestamp" => { + if not_null { + TypeValue::Time(schemapb::Time { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::Time(schemapb::Time { pos: None })) + })) + })) + } + }, + + "text" | "pg_catalog.varchar" | "pg_catalog.bpchar" | "string" | + "citext" | "name" | "uuid" | "ltree" | "lquery" | "ltxtquery" | + "varchar" | "char" | "character" | "pg_catalog.char" | "pg_catalog.character" | "bpchar" => { + if not_null { + TypeValue::String(schemapb::String { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::String(schemapb::String { pos: None })) + })) + })) + } + }, + + "inet" | "cidr" | "macaddr" | "macaddr8" => { + if not_null { + TypeValue::String(schemapb::String { pos: None }) + } else { + TypeValue::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(TypeValue::String(schemapb::String { pos: None })) + })) + })) + } + }, + + "box" | "circle" | "line" | "lseg" | "path" | "point" | "polygon" => { + TypeValue::String(schemapb::String { pos: None }) + }, + + "bit" | "varbit" | "pg_catalog.bit" | "pg_catalog.varbit" => { + TypeValue::String(schemapb::String { pos: None }) + }, + + "daterange" | "tsrange" | "tstzrange" | "numrange" | + "int4range" | "int8range" | + "datemultirange" | "tsmultirange" | "tstzmultirange" | + "nummultirange" | "int4multirange" | "int8multirange" => { + TypeValue::Any(schemapb::Any { pos: None }) + }, + + "void" | "any" => TypeValue::Any(schemapb::Any { pos: None }), + _ => { + // TODO: Handle enum types from catalog + TypeValue::Any(schemapb::Any { pos: None }) + } + }; + + schemapb::Type { + value: Some(value), + } +} diff --git a/sqlc-gen-ftl/src/protos/xyz.block.ftl.query.v1.rs b/sqlc-gen-ftl/src/protos/xyz.block.ftl.query.v1.rs new file mode 100644 index 0000000000..143e3ad528 --- /dev/null +++ b/sqlc-gen-ftl/src/protos/xyz.block.ftl.query.v1.rs @@ -0,0 +1,173 @@ +// @generated +// This file is @generated by prost-build. +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct BeginTransactionRequest { +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BeginTransactionResponse { + #[prost(string, tag="1")] + pub transaction_id: ::prost::alloc::string::String, + #[prost(enumeration="TransactionStatus", tag="2")] + pub status: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CommitTransactionRequest { + #[prost(string, tag="1")] + pub transaction_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct CommitTransactionResponse { + #[prost(enumeration="TransactionStatus", tag="1")] + pub status: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RollbackTransactionRequest { + #[prost(string, tag="1")] + pub transaction_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RollbackTransactionResponse { + #[prost(enumeration="TransactionStatus", tag="1")] + pub status: i32, +} +/// A value that can be used as a SQL parameter +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SqlValue { + #[prost(oneof="sql_value::Value", tags="1, 2, 3, 4, 5, 6, 7")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `SQLValue`. +pub mod sql_value { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(string, tag="1")] + StringValue(::prost::alloc::string::String), + #[prost(int64, tag="2")] + IntValue(i64), + #[prost(double, tag="3")] + FloatValue(f64), + #[prost(bool, tag="4")] + BoolValue(bool), + #[prost(bytes, tag="5")] + BytesValue(::prost::bytes::Bytes), + #[prost(message, tag="6")] + TimestampValue(::prost_types::Timestamp), + /// Set to true to represent NULL + #[prost(bool, tag="7")] + NullValue(bool), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExecuteQueryRequest { + #[prost(string, tag="1")] + pub raw_sql: ::prost::alloc::string::String, + #[prost(enumeration="CommandType", tag="2")] + pub command_type: i32, + /// SQL parameter values in order + #[prost(message, repeated, tag="3")] + pub parameters: ::prost::alloc::vec::Vec, + /// Column names to scan for the result type + #[prost(string, repeated, tag="6")] + pub result_columns: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, optional, tag="4")] + pub transaction_id: ::core::option::Option<::prost::alloc::string::String>, + /// Default 100 if not set + #[prost(int32, optional, tag="5")] + pub batch_size: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExecuteQueryResponse { + #[prost(oneof="execute_query_response::Result", tags="1, 2")] + pub result: ::core::option::Option, +} +/// Nested message and enum types in `ExecuteQueryResponse`. +pub mod execute_query_response { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + /// For EXEC commands + #[prost(message, tag="1")] + ExecResult(super::ExecResult), + /// For ONE/MANY commands + #[prost(message, tag="2")] + RowResults(super::RowResults), + } +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct ExecResult { + #[prost(int64, tag="1")] + pub rows_affected: i64, + /// Only for some databases like MySQL + #[prost(int64, optional, tag="2")] + pub last_insert_id: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RowResults { + /// Each row is a map of column name to value + #[prost(map="string, message", tag="1")] + pub rows: ::std::collections::HashMap<::prost::alloc::string::String, SqlValue>, + /// Indicates if there are more rows to fetch + #[prost(bool, tag="2")] + pub has_more: bool, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum TransactionStatus { + Unspecified = 0, + Success = 1, + Failed = 2, +} +impl TransactionStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "TRANSACTION_STATUS_UNSPECIFIED", + Self::Success => "TRANSACTION_STATUS_SUCCESS", + Self::Failed => "TRANSACTION_STATUS_FAILED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "TRANSACTION_STATUS_UNSPECIFIED" => Some(Self::Unspecified), + "TRANSACTION_STATUS_SUCCESS" => Some(Self::Success), + "TRANSACTION_STATUS_FAILED" => Some(Self::Failed), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum CommandType { + Unspecified = 0, + Exec = 1, + One = 2, + Many = 3, +} +impl CommandType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "COMMAND_TYPE_UNSPECIFIED", + Self::Exec => "COMMAND_TYPE_EXEC", + Self::One => "COMMAND_TYPE_ONE", + Self::Many => "COMMAND_TYPE_MANY", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "COMMAND_TYPE_UNSPECIFIED" => Some(Self::Unspecified), + "COMMAND_TYPE_EXEC" => Some(Self::Exec), + "COMMAND_TYPE_ONE" => Some(Self::One), + "COMMAND_TYPE_MANY" => Some(Self::Many), + _ => None, + } + } +} +// @@protoc_insertion_point(module) diff --git a/sqlc-gen-ftl/test/sqlc_gen_ftl_test.rs b/sqlc-gen-ftl/test/sqlc_gen_ftl_test.rs index 8c46cda1ad..2a49eba5df 100644 --- a/sqlc-gen-ftl/test/sqlc_gen_ftl_test.rs +++ b/sqlc-gen-ftl/test/sqlc_gen_ftl_test.rs @@ -23,19 +23,102 @@ fn build_wasm() -> Result<(), Box> { Ok(()) } +fn get_test_queries(engine: &str) -> Vec { + match engine { + "mysql" => vec![ + "SELECT id, big_int, small_int, some_decimal, some_numeric, some_float, some_double, some_varchar, some_text, some_char, nullable_text, some_bool, nullable_bool, some_date, some_time, some_timestamp, some_blob, some_json FROM all_types WHERE id = ?".to_string(), + "INSERT INTO all_types ( big_int, small_int, some_decimal, some_numeric, some_float, some_double, some_varchar, some_text, some_char, nullable_text, some_bool, nullable_bool, some_date, some_time, some_timestamp, some_blob, some_json ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )".to_string(), + ], + "postgresql" => vec![ + "SELECT id, big_int, small_int, some_decimal, some_numeric, some_float, some_double, some_varchar, some_text, some_char, nullable_text, some_bool, nullable_bool, some_date, some_time, some_timestamp, some_blob, some_json FROM all_types WHERE id = $1".to_string(), + "INSERT INTO all_types ( big_int, small_int, some_decimal, some_numeric, some_float, some_double, some_varchar, some_text, some_char, nullable_text, some_bool, nullable_bool, some_date, some_time, some_timestamp, some_blob, some_json ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 )".to_string(), + ], + _ => vec![], + } +} + +fn get_sqlc_config(wasm_path: &PathBuf, engine: &str) -> Result> { + let wasm_contents = fs::read(wasm_path)?; + let mut hasher = Sha256::new(); + hasher.update(&wasm_contents); + let sha256_hash = hex::encode(hasher.finalize()); + + Ok(format!( + r#"version: '2' +plugins: +- name: ftl + wasm: + url: file://sqlc-gen-ftl.wasm + sha256: {} +sql: +- schema: schema.sql + queries: queries.sql + engine: {} + codegen: + - out: gen + plugin: ftl + options: + module: echo"#, + sha256_hash, + engine, + )) +} + fn expected_module_schema(engine: &str) -> schemapb::Module { - let (param1, param2) = match engine { - "postgresql" => ("$1", "$2"), - "mysql" => ("?", "?"), - _ => panic!("Unsupported engine: {}", engine), + let queries = get_test_queries(engine); + let fields = vec![ + ("bigInt", schemapb::r#type::Value::Int(schemapb::Int { pos: None }), "big_int"), + ("smallInt", schemapb::r#type::Value::Int(schemapb::Int { pos: None }), "small_int"), + ("someDecimal", schemapb::r#type::Value::String(schemapb::String { pos: None }), "some_decimal"), + ("someNumeric", schemapb::r#type::Value::String(schemapb::String { pos: None }), "some_numeric"), + ("someFloat", schemapb::r#type::Value::Float(schemapb::Float { pos: None }), "some_float"), + ("someDouble", schemapb::r#type::Value::Float(schemapb::Float { pos: None }), "some_double"), + ("someVarchar", schemapb::r#type::Value::String(schemapb::String { pos: None }), "some_varchar"), + ("someText", schemapb::r#type::Value::String(schemapb::String { pos: None }), "some_text"), + ("someChar", schemapb::r#type::Value::String(schemapb::String { pos: None }), "some_char"), + ("nullableText", schemapb::r#type::Value::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) + })) + })), "nullable_text"), + ("someBool", schemapb::r#type::Value::Bool(schemapb::Bool { pos: None }), "some_bool"), + ("nullableBool", schemapb::r#type::Value::Optional(Box::new(schemapb::Optional { + pos: None, + r#type: Some(Box::new(schemapb::Type { + value: Some(schemapb::r#type::Value::Bool(schemapb::Bool { pos: None })) + })) + })), "nullable_bool"), + ("someDate", schemapb::r#type::Value::Time(schemapb::Time { pos: None }), "some_date"), + ("someTime", schemapb::r#type::Value::Time(schemapb::Time { pos: None }), "some_time"), + ("someTimestamp", schemapb::r#type::Value::Time(schemapb::Time { pos: None }), "some_timestamp"), + ("someBlob", schemapb::r#type::Value::Bytes(schemapb::Bytes { pos: None }), "some_blob"), + ("someJson", schemapb::r#type::Value::Any(schemapb::Any { pos: None }), "some_json"), + ]; + + let create_field = |name: &str, type_value: schemapb::r#type::Value, db_name: &str| { + schemapb::Field { + name: name.to_string(), + r#type: Some(schemapb::Type { + value: Some(type_value) + }), + pos: None, + comments: vec![], + metadata: vec![schemapb::Metadata { + value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { + pos: None, + table: "all_types".to_string(), + name: db_name.to_string(), + })) + }], + } }; - let queries = [ - format!("SELECT id, name, email FROM users WHERE id = {}", param1), - format!("INSERT INTO users (name, email) VALUES ({}, {})", param1, param2), - "SELECT data FROM requests".to_string(), - format!("INSERT INTO requests (data) VALUES ({})", param1), - ]; + let create_fields = |fields: &[(& str, schemapb::r#type::Value, &str)]| { + fields.iter().map(|(name, type_value, db_name)| { + create_field(name, type_value.clone(), db_name) + }).collect::>() + }; schemapb::Module { name: "echo".to_string(), @@ -45,173 +128,88 @@ fn expected_module_schema(engine: &str) -> schemapb::Module { metadata: vec![], pos: None, decls: vec![ + // CreateAllTypesQuery schemapb::Decl { value: Some(schemapb::decl::Value::Data(schemapb::Data { - name: "GetUserByIdQuery".to_string(), + name: "CreateAllTypesQuery".to_string(), export: false, type_parameters: vec![], - fields: vec![ - schemapb::Field { - name: "id".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Int(schemapb::Int { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "users".to_string(), - name: "id".to_string(), - })) - }], - } - ], + fields: create_fields(&fields), pos: None, comments: vec![], metadata: vec![], })), }, + // GetAllTypesQuery schemapb::Decl { value: Some(schemapb::decl::Value::Data(schemapb::Data { - name: "GetUserByIdResult".to_string(), + name: "GetAllTypesQuery".to_string(), export: false, type_parameters: vec![], fields: vec![ schemapb::Field { - name: "id".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Int(schemapb::Int { pos: None })) - }), pos: None, comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "users".to_string(), - name: "id".to_string(), - })) - }], - }, - schemapb::Field { - name: "name".to_string(), + name: "id".to_string(), r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) + value: Some(schemapb::r#type::Value::Int(schemapb::Int { pos: None })), }), - pos: None, - comments: vec![], metadata: vec![schemapb::Metadata { value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { pos: None, - table: "users".to_string(), - name: "name".to_string(), - })) + table: "all_types".to_string(), + name: "id".to_string(), + })), }], }, - schemapb::Field { - name: "email".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "users".to_string(), - name: "email".to_string(), - })) - }], - } ], pos: None, comments: vec![], metadata: vec![], })), }, - schemapb::Decl { - value: Some(schemapb::decl::Value::Verb(schemapb::Verb { - name: "getUserById".to_string(), - export: false, - runtime: None, - request: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { - module: "echo".to_string(), - name: "GetUserByIdQuery".to_string(), - pos: None, - type_parameters: vec![], - })) - }), - response: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { - module: "echo".to_string(), - name: "GetUserByIdResult".to_string(), - pos: None, - type_parameters: vec![], - })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::SqlQuery(schemapb::MetadataSqlQuery { - pos: None, - query: queries[0].clone(), - command: "one".to_string(), - })), - }], - })), - }, + // GetAllTypesResult schemapb::Decl { value: Some(schemapb::decl::Value::Data(schemapb::Data { - name: "CreateUserQuery".to_string(), + name: "GetAllTypesResult".to_string(), export: false, type_parameters: vec![], - fields: vec![ - schemapb::Field { - name: "name".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "users".to_string(), - name: "name".to_string(), - })) - }], - }, - schemapb::Field { - name: "email".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "users".to_string(), - name: "email".to_string(), - })) - }], - } - ], + fields: { + let mut result_fields = vec![ + schemapb::Field { + name: "id".to_string(), + r#type: Some(schemapb::Type { + value: Some(schemapb::r#type::Value::Int(schemapb::Int { pos: None })) + }), + pos: None, + comments: vec![], + metadata: vec![schemapb::Metadata { + value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { + pos: None, + table: "all_types".to_string(), + name: "id".to_string(), + })) + }], + }, + ]; + result_fields.extend(create_fields(&fields)); + result_fields + }, pos: None, comments: vec![], metadata: vec![], })), }, + // CreateAllTypes verb schemapb::Decl { value: Some(schemapb::decl::Value::Verb(schemapb::Verb { - name: "createUser".to_string(), + name: "createAllTypes".to_string(), export: false, runtime: None, request: Some(schemapb::Type { value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { module: "echo".to_string(), - name: "CreateUserQuery".to_string(), + name: "CreateAllTypesQuery".to_string(), pos: None, type_parameters: vec![], })) @@ -224,121 +222,41 @@ fn expected_module_schema(engine: &str) -> schemapb::Module { metadata: vec![schemapb::Metadata { value: Some(schemapb::metadata::Value::SqlQuery(schemapb::MetadataSqlQuery { pos: None, - query: queries[1].clone(), command: "exec".to_string(), + query: queries[1].clone(), })), }], })), }, - schemapb::Decl { - value: Some(schemapb::decl::Value::Data(schemapb::Data { - name: "GetRequestDataResult".to_string(), - export: false, - type_parameters: vec![], - fields: vec![ - schemapb::Field { - name: "data".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "requests".to_string(), - name: "data".to_string(), - })) - }], - } - ], - pos: None, - comments: vec![], - metadata: vec![], - })), - }, + // GetAllTypes verb schemapb::Decl { value: Some(schemapb::decl::Value::Verb(schemapb::Verb { - name: "getRequestData".to_string(), - export: false, - runtime: None, - request: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Unit(schemapb::Unit { pos: None })) - }), - response: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Array(Box::new(schemapb::Array { - pos: None, - element: Some(Box::new(schemapb::Type { - value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { - module: "echo".to_string(), - name: "GetRequestDataResult".to_string(), - pos: None, - type_parameters: vec![], - })) - })), - }))) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::SqlQuery(schemapb::MetadataSqlQuery { - pos: None, - query: queries[2].clone(), - command: "many".to_string(), - })), - }], - })), - }, - schemapb::Decl { - value: Some(schemapb::decl::Value::Data(schemapb::Data { - name: "CreateRequestQuery".to_string(), - export: false, - type_parameters: vec![], - fields: vec![ - schemapb::Field { - name: "data".to_string(), - r#type: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::String(schemapb::String { pos: None })) - }), - pos: None, - comments: vec![], - metadata: vec![schemapb::Metadata { - value: Some(schemapb::metadata::Value::DbColumn(schemapb::MetadataDbColumn { - pos: None, - table: "requests".to_string(), - name: "data".to_string(), - })) - }], - } - ], - pos: None, - comments: vec![], - metadata: vec![], - })), - }, - schemapb::Decl { - value: Some(schemapb::decl::Value::Verb(schemapb::Verb { - name: "createRequest".to_string(), + name: "getAllTypes".to_string(), export: false, runtime: None, request: Some(schemapb::Type { value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { module: "echo".to_string(), - name: "CreateRequestQuery".to_string(), + name: "GetAllTypesQuery".to_string(), pos: None, type_parameters: vec![], })) }), response: Some(schemapb::Type { - value: Some(schemapb::r#type::Value::Unit(schemapb::Unit { pos: None })) + value: Some(schemapb::r#type::Value::Ref(schemapb::Ref { + module: "echo".to_string(), + name: "GetAllTypesResult".to_string(), + pos: None, + type_parameters: vec![], + })) }), pos: None, comments: vec![], metadata: vec![schemapb::Metadata { value: Some(schemapb::metadata::Value::SqlQuery(schemapb::MetadataSqlQuery { pos: None, - query: queries[3].clone(), - command: "exec".to_string(), + command: "one".to_string(), + query: queries[0].clone(), })), }], })), @@ -347,33 +265,6 @@ fn expected_module_schema(engine: &str) -> schemapb::Module { } } -fn get_sqlc_config(wasm_path: &PathBuf, engine: &str) -> Result> { - let wasm_contents = fs::read(wasm_path)?; - let mut hasher = Sha256::new(); - hasher.update(&wasm_contents); - let sha256_hash = hex::encode(hasher.finalize()); - - Ok(format!( - r#"version: '2' -plugins: -- name: ftl - wasm: - url: file://sqlc-gen-ftl.wasm - sha256: {} -sql: -- schema: schema.sql - queries: queries.sql - engine: {} - codegen: - - out: gen - plugin: ftl - options: - module: echo"#, - sha256_hash, - engine, - )) -} - #[cfg(test)] mod tests { use super::*; @@ -428,8 +319,36 @@ mod tests { } let pb_contents = std::fs::read(gen_dir.join("queries.pb"))?; - let actual_module = schemapb::Module::decode(&*pb_contents)?; - let expected_module = expected_module_schema(engine); + let mut actual_module = schemapb::Module::decode(&*pb_contents)?; + let mut expected_module = expected_module_schema(engine); + + // Sort declarations by name before comparison + actual_module.decls.sort_by(|a, b| { + let name_a = match &a.value { + Some(schemapb::decl::Value::Data(d)) => &d.name, + Some(schemapb::decl::Value::Verb(v)) => &v.name, + _ => "", + }; + let name_b = match &b.value { + Some(schemapb::decl::Value::Data(d)) => &d.name, + Some(schemapb::decl::Value::Verb(v)) => &v.name, + _ => "", + }; + name_a.cmp(name_b) + }); + expected_module.decls.sort_by(|a, b| { + let name_a = match &a.value { + Some(schemapb::decl::Value::Data(d)) => &d.name, + Some(schemapb::decl::Value::Verb(v)) => &v.name, + _ => "", + }; + let name_b = match &b.value { + Some(schemapb::decl::Value::Data(d)) => &d.name, + Some(schemapb::decl::Value::Verb(v)) => &v.name, + _ => "", + }; + name_a.cmp(name_b) + }); assert_eq!( &actual_module, diff --git a/sqlc-gen-ftl/test/testdata/mysql/queries.sql b/sqlc-gen-ftl/test/testdata/mysql/queries.sql index 82ccf3b492..6594a01c5e 100644 --- a/sqlc-gen-ftl/test/testdata/mysql/queries.sql +++ b/sqlc-gen-ftl/test/testdata/mysql/queries.sql @@ -1,12 +1,15 @@ --- name: GetUserByID :one -SELECT id, name, email FROM users WHERE id = ?; - --- name: CreateUser :exec -INSERT INTO users (name, email) VALUES (?, ?); - --- name: GetRequestData :many -SELECT data FROM requests; - --- name: CreateRequest :exec -INSERT INTO requests (data) VALUES (?); +-- name: GetAllTypes :one +SELECT * FROM all_types WHERE id = ?; + +-- name: CreateAllTypes :exec +INSERT INTO all_types ( + big_int, small_int, + some_decimal, some_numeric, some_float, some_double, + some_varchar, some_text, some_char, nullable_text, + some_bool, nullable_bool, + some_date, some_time, some_timestamp, + some_blob, some_json +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +); diff --git a/sqlc-gen-ftl/test/testdata/mysql/schema.sql b/sqlc-gen-ftl/test/testdata/mysql/schema.sql index d62ff06957..ef26c72d7b 100644 --- a/sqlc-gen-ftl/test/testdata/mysql/schema.sql +++ b/sqlc-gen-ftl/test/testdata/mysql/schema.sql @@ -1,11 +1,33 @@ -CREATE TABLE users ( +CREATE TABLE all_types ( + -- Integer types id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - email TEXT -); - -CREATE TABLE requests -( - data TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + big_int BIGINT NOT NULL, + small_int SMALLINT NOT NULL, + + -- Numeric/Decimal types + some_decimal DECIMAL(10,2) NOT NULL, + some_numeric NUMERIC(10,2) NOT NULL, + some_float FLOAT NOT NULL, + some_double DOUBLE PRECISION NOT NULL, + + -- Character types + some_varchar VARCHAR(255) NOT NULL, + some_text TEXT NOT NULL, + some_char CHAR(10) NOT NULL, + nullable_text TEXT, + + -- Boolean type + some_bool BOOLEAN NOT NULL, + nullable_bool BOOLEAN, + + -- Date/Time types + some_date DATE NOT NULL, + some_time TIME NOT NULL, + some_timestamp TIMESTAMP NOT NULL, + + -- Binary type + some_blob BLOB NOT NULL, + + -- JSON type + some_json JSON NOT NULL ); diff --git a/sqlc-gen-ftl/test/testdata/postgresql/queries.sql b/sqlc-gen-ftl/test/testdata/postgresql/queries.sql index fb07f90971..9d946dfc3f 100644 --- a/sqlc-gen-ftl/test/testdata/postgresql/queries.sql +++ b/sqlc-gen-ftl/test/testdata/postgresql/queries.sql @@ -1,12 +1,15 @@ --- name: GetUserByID :one -SELECT id, name, email FROM users WHERE id = $1; - --- name: CreateUser :exec -INSERT INTO users (name, email) VALUES ($1, $2); - --- name: GetRequestData :many -SELECT data FROM requests; - --- name: CreateRequest :exec -INSERT INTO requests (data) VALUES ($1); +-- name: GetAllTypes :one +SELECT * FROM all_types WHERE id = $1; + +-- name: CreateAllTypes :exec +INSERT INTO all_types ( + big_int, small_int, + some_decimal, some_numeric, some_float, some_double, + some_varchar, some_text, some_char, nullable_text, + some_bool, nullable_bool, + some_date, some_time, some_timestamp, + some_blob, some_json +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 +); diff --git a/sqlc-gen-ftl/test/testdata/postgresql/schema.sql b/sqlc-gen-ftl/test/testdata/postgresql/schema.sql index d62ff06957..6432f0292f 100644 --- a/sqlc-gen-ftl/test/testdata/postgresql/schema.sql +++ b/sqlc-gen-ftl/test/testdata/postgresql/schema.sql @@ -1,11 +1,33 @@ -CREATE TABLE users ( +CREATE TABLE all_types ( + -- Integer types id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - email TEXT -); - -CREATE TABLE requests -( - data TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + big_int BIGINT NOT NULL, + small_int SMALLINT NOT NULL, + + -- Numeric/Decimal types + some_decimal DECIMAL(10,2) NOT NULL, + some_numeric NUMERIC(10,2) NOT NULL, + some_float FLOAT NOT NULL, + some_double DOUBLE PRECISION NOT NULL, + + -- Character types + some_varchar VARCHAR(255) NOT NULL, + some_text TEXT NOT NULL, + some_char CHAR(10) NOT NULL, + nullable_text TEXT, + + -- Boolean type + some_bool BOOLEAN NOT NULL, + nullable_bool BOOLEAN, + + -- Date/Time types + some_date DATE NOT NULL, + some_time TIME NOT NULL, + some_timestamp TIMESTAMP NOT NULL, + + -- Binary type + some_blob BYTEA NOT NULL, + + -- JSON type + some_json JSON NOT NULL );