diff --git a/docker-compose.yml b/docker-compose.yml index c7d9c50db0..a751603965 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,8 +27,10 @@ services: - "2136:2136" - "8765:8765" restart: always + hostname: localhost environment: - YDB_USE_IN_MEMORY_PDISKS=true - GRPC_TLS_PORT=2135 - GRPC_PORT=2136 - MON_PORT=8765 + diff --git a/examples/authors/sqlc.yaml b/examples/authors/sqlc.yaml index 143cb608b0..49fe62ff76 100644 --- a/examples/authors/sqlc.yaml +++ b/examples/authors/sqlc.yaml @@ -52,6 +52,7 @@ sql: package: authors out: ydb emit_json_tags: true + sql_package: ydb-go-sdk rules: diff --git a/examples/authors/ydb/db.go b/examples/authors/ydb/db.go index e2b0a86b13..c3b16b4481 100644 --- a/examples/authors/ydb/db.go +++ b/examples/authors/ydb/db.go @@ -6,14 +6,15 @@ package authors import ( "context" - "database/sql" + + "github.com/ydb-platform/ydb-go-sdk/v3/query" ) type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row + Exec(ctx context.Context, sql string, opts ...query.ExecuteOption) error + Query(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.Result, error) + QueryResultSet(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.ClosableResultSet, error) + QueryRow(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.Row, error) } func New(db DBTX) *Queries { @@ -23,9 +24,3 @@ func New(db DBTX) *Queries { type Queries struct { db DBTX } - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/examples/authors/ydb/db_test.go b/examples/authors/ydb/db_test.go index 76b37306ef..6ab15913f0 100644 --- a/examples/authors/ydb/db_test.go +++ b/examples/authors/ydb/db_test.go @@ -6,6 +6,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/sqltest/local" _ "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/query" ) func ptr(s string) *string { @@ -15,10 +16,10 @@ func ptr(s string) *string { func TestAuthors(t *testing.T) { ctx := context.Background() - test := local.YDB(t, []string{"schema.sql"}) - defer test.DB.Close() + db := local.YDB(t, []string{"schema.sql"}) + defer db.Close(ctx) - q := New(test.DB) + q := New(db.Query()) t.Run("InsertAuthors", func(t *testing.T) { authorsToInsert := []CreateOrUpdateAuthorParams{ @@ -38,53 +39,12 @@ func TestAuthors(t *testing.T) { } for _, author := range authorsToInsert { - if _, err := q.CreateOrUpdateAuthor(ctx, author); err != nil { + if err := q.CreateOrUpdateAuthor(ctx, author, query.WithIdempotent()); err != nil { t.Fatalf("failed to insert author %q: %v", author.P1, err) } } }) - t.Run("CreateOrUpdateAuthorReturningBio", func(t *testing.T) { - newBio := "Обновленная биография автора" - arg := CreateOrUpdateAuthorReturningBioParams{ - P0: 3, - P1: "Тестовый Автор", - P2: &newBio, - } - - returnedBio, err := q.CreateOrUpdateAuthorReturningBio(ctx, arg) - if err != nil { - t.Fatalf("failed to create or update author: %v", err) - } - - if returnedBio == nil { - t.Fatal("expected non-nil bio, got nil") - } - if *returnedBio != newBio { - t.Fatalf("expected bio %q, got %q", newBio, *returnedBio) - } - - t.Logf("Author created or updated successfully with bio: %s", *returnedBio) - }) - - t.Run("Update Author", func(t *testing.T) { - arg := UpdateAuthorByIDParams{ - P0: "Максим Горький", - P1: ptr("Обновленная биография"), - P2: 10, - } - - singleAuthor, err := q.UpdateAuthorByID(ctx, arg) - if err != nil { - t.Fatal(err) - } - bio := "Null" - if singleAuthor.Bio != nil { - bio = *singleAuthor.Bio - } - t.Logf("- ID: %d | Name: %s | Bio: %s", singleAuthor.ID, singleAuthor.Name, bio) - }) - t.Run("ListAuthors", func(t *testing.T) { authors, err := q.ListAuthors(ctx) if err != nil { @@ -115,46 +75,10 @@ func TestAuthors(t *testing.T) { t.Logf("- ID: %d | Name: %s | Bio: %s", singleAuthor.ID, singleAuthor.Name, bio) }) - t.Run("GetAuthorByName", func(t *testing.T) { - authors, err := q.GetAuthorsByName(ctx, "Александр Пушкин") - if err != nil { - t.Fatal(err) - } - if len(authors) == 0 { - t.Fatal("expected at least one author with this name, got none") - } - t.Log("Authors with this name:") - for _, a := range authors { - bio := "Null" - if a.Bio != nil { - bio = *a.Bio - } - t.Logf("- ID: %d | Name: %s | Bio: %s", a.ID, a.Name, bio) - } - }) - - t.Run("ListAuthorsWithNullBio", func(t *testing.T) { - authors, err := q.ListAuthorsWithNullBio(ctx) - if err != nil { - t.Fatal(err) - } - if len(authors) == 0 { - t.Fatal("expected at least one author with NULL bio, got none") - } - t.Log("Authors with NULL bio:") - for _, a := range authors { - bio := "Null" - if a.Bio != nil { - bio = *a.Bio - } - t.Logf("- ID: %d | Name: %s | Bio: %s", a.ID, a.Name, bio) - } - }) - t.Run("Delete All Authors", func(t *testing.T) { var i uint64 for i = 1; i <= 13; i++ { - if err := q.DeleteAuthor(ctx, i); err != nil { + if err := q.DeleteAuthor(ctx, i, query.WithIdempotent()); err != nil { t.Fatalf("failed to delete authors: %v", err) } } @@ -166,4 +90,11 @@ func TestAuthors(t *testing.T) { t.Fatalf("expected no authors, got %d", len(authors)) } }) + + t.Run("Drop Table Authors", func(t *testing.T) { + err := q.DropTable(ctx) + if err != nil { + t.Fatal(err) + } + }) } diff --git a/examples/authors/ydb/query.sql b/examples/authors/ydb/query.sql index bf672042c5..804150615d 100644 --- a/examples/authors/ydb/query.sql +++ b/examples/authors/ydb/query.sql @@ -1,32 +1,15 @@ --- name: ListAuthors :many -SELECT * FROM authors; - -- name: GetAuthor :one SELECT * FROM authors -WHERE id = $p0; +WHERE id = $p0 LIMIT 1; --- name: GetAuthorsByName :many -SELECT * FROM authors -WHERE name = $p0; - --- name: ListAuthorsWithNullBio :many -SELECT * FROM authors -WHERE bio IS NULL; - --- name: Count :one -SELECT COUNT(*) FROM authors; - --- name: COALESCE :many -SELECT id, name, COALESCE(bio, 'Null value!') FROM authors; +-- name: ListAuthors :many +SELECT * FROM authors ORDER BY name; --- name: CreateOrUpdateAuthor :execresult +-- name: CreateOrUpdateAuthor :exec UPSERT INTO authors (id, name, bio) VALUES ($p0, $p1, $p2); --- name: CreateOrUpdateAuthorReturningBio :one -UPSERT INTO authors (id, name, bio) VALUES ($p0, $p1, $p2) RETURNING bio; - -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $p0; --- name: UpdateAuthorByID :one -UPDATE authors SET name = $p0, bio = $p1 WHERE id = $p2 RETURNING *; +-- name: DropTable :exec +DROP TABLE IF EXISTS authors; \ No newline at end of file diff --git a/examples/authors/ydb/query.sql.go b/examples/authors/ydb/query.sql.go index 45d86c96fd..7459482b3a 100644 --- a/examples/authors/ydb/query.sql.go +++ b/examples/authors/ydb/query.sql.go @@ -7,54 +7,13 @@ package authors import ( "context" - "database/sql" -) - -const cOALESCE = `-- name: COALESCE :many -SELECT id, name, COALESCE(bio, 'Null value!') FROM authors -` - -type COALESCERow struct { - ID uint64 `json:"id"` - Name string `json:"name"` - Bio string `json:"bio"` -} -func (q *Queries) COALESCE(ctx context.Context) ([]COALESCERow, error) { - rows, err := q.db.QueryContext(ctx, cOALESCE) - if err != nil { - return nil, err - } - defer rows.Close() - var items []COALESCERow - for rows.Next() { - var i COALESCERow - if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const count = `-- name: Count :one -SELECT COUNT(*) FROM authors -` - -func (q *Queries) Count(ctx context.Context) (uint64, error) { - row := q.db.QueryRowContext(ctx, count) - var count uint64 - err := row.Scan(&count) - return count, err -} + "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xerrors" + "github.com/ydb-platform/ydb-go-sdk/v3/query" +) -const createOrUpdateAuthor = `-- name: CreateOrUpdateAuthor :execresult +const createOrUpdateAuthor = `-- name: CreateOrUpdateAuthor :exec UPSERT INTO authors (id, name, bio) VALUES ($p0, $p1, $p2) ` @@ -64,144 +23,97 @@ type CreateOrUpdateAuthorParams struct { P2 *string `json:"p2"` } -func (q *Queries) CreateOrUpdateAuthor(ctx context.Context, arg CreateOrUpdateAuthorParams) (sql.Result, error) { - return q.db.ExecContext(ctx, createOrUpdateAuthor, arg.P0, arg.P1, arg.P2) -} - -const createOrUpdateAuthorReturningBio = `-- name: CreateOrUpdateAuthorReturningBio :one -UPSERT INTO authors (id, name, bio) VALUES ($p0, $p1, $p2) RETURNING bio -` - -type CreateOrUpdateAuthorReturningBioParams struct { - P0 uint64 `json:"p0"` - P1 string `json:"p1"` - P2 *string `json:"p2"` -} - -func (q *Queries) CreateOrUpdateAuthorReturningBio(ctx context.Context, arg CreateOrUpdateAuthorReturningBioParams) (*string, error) { - row := q.db.QueryRowContext(ctx, createOrUpdateAuthorReturningBio, arg.P0, arg.P1, arg.P2) - var bio *string - err := row.Scan(&bio) - return bio, err +func (q *Queries) CreateOrUpdateAuthor(ctx context.Context, arg CreateOrUpdateAuthorParams, opts ...query.ExecuteOption) error { + err := q.db.Exec(ctx, createOrUpdateAuthor, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ + "$p0": arg.P0, + "$p1": arg.P1, + "$p2": arg.P2, + })))..., + ) + if err != nil { + return xerrors.WithStackTrace(err) + } + return nil } const deleteAuthor = `-- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $p0 ` -func (q *Queries) DeleteAuthor(ctx context.Context, p0 uint64) error { - _, err := q.db.ExecContext(ctx, deleteAuthor, p0) - return err -} - -const getAuthor = `-- name: GetAuthor :one -SELECT id, name, bio FROM authors -WHERE id = $p0 -` - -func (q *Queries) GetAuthor(ctx context.Context, p0 uint64) (Author, error) { - row := q.db.QueryRowContext(ctx, getAuthor, p0) - var i Author - err := row.Scan(&i.ID, &i.Name, &i.Bio) - return i, err +func (q *Queries) DeleteAuthor(ctx context.Context, p0 uint64, opts ...query.ExecuteOption) error { + err := q.db.Exec(ctx, deleteAuthor, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ + "$p0": p0, + })))..., + ) + if err != nil { + return xerrors.WithStackTrace(err) + } + return nil } -const getAuthorsByName = `-- name: GetAuthorsByName :many -SELECT id, name, bio FROM authors -WHERE name = $p0 +const dropTable = `-- name: DropTable :exec +DROP TABLE IF EXISTS authors ` -func (q *Queries) GetAuthorsByName(ctx context.Context, p0 string) ([]Author, error) { - rows, err := q.db.QueryContext(ctx, getAuthorsByName, p0) +func (q *Queries) DropTable(ctx context.Context, opts ...query.ExecuteOption) error { + err := q.db.Exec(ctx, dropTable, opts...) if err != nil { - return nil, err - } - defer rows.Close() - var items []Author - for rows.Next() { - var i Author - if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err + return xerrors.WithStackTrace(err) } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil + return nil } -const listAuthors = `-- name: ListAuthors :many +const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors +WHERE id = $p0 LIMIT 1 ` -func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { - rows, err := q.db.QueryContext(ctx, listAuthors) +func (q *Queries) GetAuthor(ctx context.Context, p0 uint64, opts ...query.ExecuteOption) (Author, error) { + row, err := q.db.QueryRow(ctx, getAuthor, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ + "$p0": p0, + })))..., + ) + var i Author if err != nil { - return nil, err - } - defer rows.Close() - var items []Author - for rows.Next() { - var i Author - if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { - return nil, err - } - items = append(items, i) + return i, xerrors.WithStackTrace(err) } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err + err = row.Scan(&i.ID, &i.Name, &i.Bio) + if err != nil { + return i, xerrors.WithStackTrace(err) } - return items, nil + return i, nil } -const listAuthorsWithNullBio = `-- name: ListAuthorsWithNullBio :many -SELECT id, name, bio FROM authors -WHERE bio IS NULL +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio FROM authors ORDER BY name ` -func (q *Queries) ListAuthorsWithNullBio(ctx context.Context) ([]Author, error) { - rows, err := q.db.QueryContext(ctx, listAuthorsWithNullBio) +func (q *Queries) ListAuthors(ctx context.Context, opts ...query.ExecuteOption) ([]Author, error) { + result, err := q.db.Query(ctx, listAuthors, opts...) if err != nil { - return nil, err + return nil, xerrors.WithStackTrace(err) } - defer rows.Close() var items []Author - for rows.Next() { - var i Author - if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { - return nil, err + for set, err := range result.ResultSets(ctx) { + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + for row, err := range set.Rows(ctx) { + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + var i Author + if err := row.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, xerrors.WithStackTrace(err) + } + items = append(items, i) } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err } - if err := rows.Err(); err != nil { - return nil, err + if err := result.Close(ctx); err != nil { + return nil, xerrors.WithStackTrace(err) } return items, nil } - -const updateAuthorByID = `-- name: UpdateAuthorByID :one -UPDATE authors SET name = $p0, bio = $p1 WHERE id = $p2 RETURNING id, name, bio -` - -type UpdateAuthorByIDParams struct { - P0 string `json:"p0"` - P1 *string `json:"p1"` - P2 uint64 `json:"p2"` -} - -func (q *Queries) UpdateAuthorByID(ctx context.Context, arg UpdateAuthorByIDParams) (Author, error) { - row := q.db.QueryRowContext(ctx, updateAuthorByID, arg.P0, arg.P1, arg.P2) - var i Author - err := row.Scan(&i.ID, &i.Name, &i.Bio) - return i, err -} diff --git a/go.mod b/go.mod index b5a03ba6c2..57a5640449 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/tetratelabs/wazero v1.9.0 github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 github.com/xeipuuv/gojsonschema v1.2.0 - github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 + github.com/ydb-platform/ydb-go-sdk/v3 v3.115.3 github.com/ydb-platform/yql-parsers v0.0.0-20250309001738-7d693911f333 golang.org/x/sync v0.13.0 google.golang.org/grpc v1.72.0 @@ -48,7 +48,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jonboulle/clockwork v0.3.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect diff --git a/go.sum b/go.sum index c5615aa00f..5f57a8acf9 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -241,6 +243,8 @@ github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 h1:TwWSp3gRMcja/hRpOofncLvgxAXCmzpz5cGtmdaoITw= github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho= +github.com/ydb-platform/ydb-go-sdk/v3 v3.115.3 h1:SFeSK2c+PmiToyNIhr143u+YDzLhl/kboXwKLYDk0O4= +github.com/ydb-platform/ydb-go-sdk/v3 v3.115.3/go.mod h1:Pp1w2xxUoLQ3NCNAwV7pvDq0TVQOdtAqs+ZiC+i8r14= github.com/ydb-platform/yql-parsers v0.0.0-20250309001738-7d693911f333 h1:KFtJwlPdOxWjCKXX0jFJ8k1FlbqbRbUW3k/kYSZX7SA= github.com/ydb-platform/yql-parsers v0.0.0-20250309001738-7d693911f333/go.mod h1:vrPJPS8cdPSV568YcXhB4bUwhyV8bmWKqmQ5c5Xi99o= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= diff --git a/internal/codegen/golang/driver.go b/internal/codegen/golang/driver.go index 5e3a533dcc..6e0596172f 100644 --- a/internal/codegen/golang/driver.go +++ b/internal/codegen/golang/driver.go @@ -8,6 +8,8 @@ func parseDriver(sqlPackage string) opts.SQLDriver { return opts.SQLDriverPGXV4 case opts.SQLPackagePGXV5: return opts.SQLDriverPGXV5 + case opts.SQLPackageYDBGoSDK: + return opts.SQLDriverYDBGoSDK default: return opts.SQLDriverLibPQ } diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index ac91cc537f..35f76b90e7 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -209,6 +209,15 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, return nil, errors.New(":batch* commands are only supported by pgx") } + if tctx.SQLDriver.IsYDBGoSDK() { + for _, q := range queries { + switch q.Cmd { + case metadata.CmdExecResult, metadata.CmdExecRows, metadata.CmdExecLastId: + return nil, fmt.Errorf("%s is not supported by ydb-go-sdk", q.Cmd) + } + } + } + funcMap := template.FuncMap{ "lowerTitle": sdk.LowerTitle, "comment": sdk.DoubleSlashComment, diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index ccca4f603c..17e426b8f9 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -132,6 +132,8 @@ func (i *importer) dbImports() fileImports { case opts.SQLDriverPGXV5: pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}) pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5"}) + case opts.SQLDriverYDBGoSDK: + pkg = append(pkg, ImportSpec{Path: "github.com/ydb-platform/ydb-go-sdk/v3/query"}) default: std = append(std, ImportSpec{Path: "database/sql"}) if i.Options.EmitPreparedQueries { @@ -177,7 +179,9 @@ func buildImports(options *opts.Options, queries []Query, uses func(string) bool case opts.SQLDriverPGXV5: pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}] = struct{}{} default: - std["database/sql"] = struct{}{} + if !sqlpkg.IsYDBGoSDK() { + std["database/sql"] = struct{}{} + } } } } @@ -267,6 +271,11 @@ func (i *importer) interfaceImports() fileImports { }) std["context"] = struct{}{} + + sqlpkg := parseDriver(i.Options.SqlPackage) + if sqlpkg.IsYDBGoSDK() { + pkg[ImportSpec{Path: "github.com/ydb-platform/ydb-go-sdk/v3/query"}] = struct{}{} + } return sortedImports(std, pkg) } @@ -395,13 +404,28 @@ func (i *importer) queryImports(filename string) fileImports { } sqlpkg := parseDriver(i.Options.SqlPackage) - if sqlcSliceScan() && !sqlpkg.IsPGX() { + if sqlcSliceScan() && !sqlpkg.IsPGX() && !sqlpkg.IsYDBGoSDK() { std["strings"] = struct{}{} } - if sliceScan() && !sqlpkg.IsPGX() { + if sliceScan() && !sqlpkg.IsPGX() && !sqlpkg.IsYDBGoSDK() { pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{} } + if sqlpkg.IsYDBGoSDK() { + hasParams := false + for _, q := range gq { + if !q.Arg.isEmpty() { + hasParams = true + break + } + } + if hasParams { + pkg[ImportSpec{Path: "github.com/ydb-platform/ydb-go-sdk/v3"}] = struct{}{} + } + pkg[ImportSpec{Path: "github.com/ydb-platform/ydb-go-sdk/v3/query"}] = struct{}{} + pkg[ImportSpec{Path: "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xerrors"}] = struct{}{} + } + if i.Options.WrapErrors { std["fmt"] = struct{}{} } diff --git a/internal/codegen/golang/opts/enum.go b/internal/codegen/golang/opts/enum.go index 40457d040a..4d57a080a8 100644 --- a/internal/codegen/golang/opts/enum.go +++ b/internal/codegen/golang/opts/enum.go @@ -8,12 +8,14 @@ const ( SQLPackagePGXV4 string = "pgx/v4" SQLPackagePGXV5 string = "pgx/v5" SQLPackageStandard string = "database/sql" + SQLPackageYDBGoSDK string = "ydb-go-sdk" ) var validPackages = map[string]struct{}{ string(SQLPackagePGXV4): {}, string(SQLPackagePGXV5): {}, string(SQLPackageStandard): {}, + string(SQLPackageYDBGoSDK): {}, } func validatePackage(sqlPackage string) error { @@ -28,6 +30,7 @@ const ( SQLDriverPGXV5 = "github.com/jackc/pgx/v5" SQLDriverLibPQ = "github.com/lib/pq" SQLDriverGoSQLDriverMySQL = "github.com/go-sql-driver/mysql" + SQLDriverYDBGoSDK = "github.com/ydb-platform/ydb-go-sdk/v3" ) var validDrivers = map[string]struct{}{ @@ -35,6 +38,7 @@ var validDrivers = map[string]struct{}{ string(SQLDriverPGXV5): {}, string(SQLDriverLibPQ): {}, string(SQLDriverGoSQLDriverMySQL): {}, + string(SQLDriverYDBGoSDK): {}, } func validateDriver(sqlDriver string) error { @@ -52,12 +56,18 @@ func (d SQLDriver) IsGoSQLDriverMySQL() bool { return d == SQLDriverGoSQLDriverMySQL } +func (d SQLDriver) IsYDBGoSDK() bool { + return d == SQLDriverYDBGoSDK +} + func (d SQLDriver) Package() string { switch d { case SQLDriverPGXV4: return SQLPackagePGXV4 case SQLDriverPGXV5: return SQLPackagePGXV5 + case SQLDriverYDBGoSDK: + return SQLPackageYDBGoSDK default: return SQLPackageStandard } diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 3b4fb2fa1a..02a09c3870 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -39,6 +39,10 @@ func (v QueryValue) isEmpty() bool { return v.Typ == "" && v.Name == "" && v.Struct == nil } +func (v QueryValue) IsEmpty() bool { + return v.isEmpty() +} + type Argument struct { Name string Type string @@ -254,6 +258,56 @@ func (v QueryValue) VariableForField(f Field) string { return v.Name + "." + f.Name } +func addDollarPrefix(name string) string { + if name == "" { + return name + } + if strings.HasPrefix(name, "$") { + return name + } + return "$" + name +} + +// YDBParamMapEntries returns entries for a map[string]any literal for YDB parameters. +func (v QueryValue) YDBParamMapEntries() string { + if v.isEmpty() { + return "" + } + + var parts []string + for _, field := range v.getParameterFields() { + if field.Column != nil && field.Column.IsNamedParam { + name := field.Column.GetName() + if name != "" { + key := fmt.Sprintf("%q", addDollarPrefix(name)) + variable := v.VariableForField(field) + parts = append(parts, key+": "+escape(variable)) + } + } + } + + if len(parts) == 0 { + return "" + } + + parts = append(parts, "") + return "\n" + strings.Join(parts, ",\n") +} + +func (v QueryValue) getParameterFields() []Field { + if v.Struct == nil { + return []Field{ + { + Name: v.Name, + DBName: v.DBName, + Type: v.Typ, + Column: v.Column, + }, + } + } + return v.Struct.Fields +} + // A struct used to generate methods and fields on the Queries struct type Query struct { Cmd string diff --git a/internal/codegen/golang/templates/template.tmpl b/internal/codegen/golang/templates/template.tmpl index afd50c01ac..f74b796349 100644 --- a/internal/codegen/golang/templates/template.tmpl +++ b/internal/codegen/golang/templates/template.tmpl @@ -25,6 +25,8 @@ import ( {{if .SQLDriver.IsPGX }} {{- template "dbCodeTemplatePgx" .}} +{{else if .SQLDriver.IsYDBGoSDK }} + {{- template "dbCodeTemplateYDB" .}} {{else}} {{- template "dbCodeTemplateStd" .}} {{end}} @@ -57,6 +59,8 @@ import ( {{define "interfaceCode"}} {{if .SQLDriver.IsPGX }} {{- template "interfaceCodePgx" .}} + {{else if .SQLDriver.IsYDBGoSDK }} + {{- template "interfaceCodeYDB" .}} {{else}} {{- template "interfaceCodeStd" .}} {{end}} @@ -188,6 +192,8 @@ import ( {{define "queryCode"}} {{if .SQLDriver.IsPGX }} {{- template "queryCodePgx" .}} +{{else if .SQLDriver.IsYDBGoSDK }} + {{- template "queryCodeYDB" .}} {{else}} {{- template "queryCodeStd" .}} {{end}} diff --git a/internal/codegen/golang/templates/ydb-go-sdk/dbCode.tmpl b/internal/codegen/golang/templates/ydb-go-sdk/dbCode.tmpl new file mode 100644 index 0000000000..f79831d2e2 --- /dev/null +++ b/internal/codegen/golang/templates/ydb-go-sdk/dbCode.tmpl @@ -0,0 +1,24 @@ +{{define "dbCodeTemplateYDB"}} +type DBTX interface { + Exec(ctx context.Context, sql string, opts ...query.ExecuteOption) error + Query(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.Result, error) + QueryResultSet(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.ClosableResultSet, error) + QueryRow(ctx context.Context, sql string, opts ...query.ExecuteOption) (query.Row, error) +} + +{{ if .EmitMethodsWithDBArgument}} +func New() *Queries { + return &Queries{} +{{- else -}} +func New(db DBTX) *Queries { + return &Queries{db: db} +{{- end}} +} + +type Queries struct { + {{if not .EmitMethodsWithDBArgument}} + db DBTX + {{end}} +} + +{{end}} diff --git a/internal/codegen/golang/templates/ydb-go-sdk/interfaceCode.tmpl b/internal/codegen/golang/templates/ydb-go-sdk/interfaceCode.tmpl new file mode 100644 index 0000000000..f9c06cc705 --- /dev/null +++ b/internal/codegen/golang/templates/ydb-go-sdk/interfaceCode.tmpl @@ -0,0 +1,36 @@ +{{define "interfaceCodeYDB"}} + type Querier interface { + {{- $dbtxParam := .EmitMethodsWithDBArgument -}} + {{- range .GoQueries}} + {{- if and (eq .Cmd ":one") ($dbtxParam) }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ({{.Ret.DefineType}}, error) + {{- else if eq .Cmd ":one"}} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ({{.Ret.DefineType}}, error) + {{- end}} + {{- if and (eq .Cmd ":many") ($dbtxParam) }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ([]{{.Ret.DefineType}}, error) + {{- else if eq .Cmd ":many"}} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ([]{{.Ret.DefineType}}, error) + {{- end}} + {{- if and (eq .Cmd ":exec") ($dbtxParam) }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) error + {{- else if eq .Cmd ":exec"}} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) error + {{- end}} + {{- end}} + } + + var _ Querier = (*Queries)(nil) +{{end}} diff --git a/internal/codegen/golang/templates/ydb-go-sdk/queryCode.tmpl b/internal/codegen/golang/templates/ydb-go-sdk/queryCode.tmpl new file mode 100644 index 0000000000..ecd78b1344 --- /dev/null +++ b/internal/codegen/golang/templates/ydb-go-sdk/queryCode.tmpl @@ -0,0 +1,145 @@ +{{define "queryCodeYDB"}} +{{range .GoQueries}} +{{if $.OutputQuery .SourceName}} +const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} +{{escape .SQL}} +{{$.Q}} + +{{if .Arg.EmitStruct}} +type {{.Arg.Type}} struct { {{- range .Arg.UniqueFields}} + {{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} + +{{if .Ret.EmitStruct}} +type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} + {{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} + +{{if eq .Cmd ":one"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{if $.EmitMethodsWithDBArgument}}db DBTX, {{end}}{{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ({{.Ret.DefineType}}, error) { + {{- $dbArg := "q.db" }}{{- if $.EmitMethodsWithDBArgument }}{{- $dbArg = "db" }}{{- end -}} + {{- if .Arg.IsEmpty }} + row, err := {{$dbArg}}.QueryRow(ctx, {{.ConstantName}}, opts...) + {{- else }} + row, err := {{$dbArg}}.QueryRow(ctx, {{.ConstantName}}, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ {{.Arg.YDBParamMapEntries}} })))..., + ) + {{- end }} + {{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }} + var {{.Ret.Name}} {{.Ret.Type}} + {{- end}} + if err != nil { + {{- if $.WrapErrors}} + return {{.Ret.ReturnName}}, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return {{.Ret.ReturnName}}, xerrors.WithStackTrace(err) + {{- end }} + } + err = row.Scan({{.Ret.Scan}}) + {{- if $.WrapErrors}} + if err != nil { + return {{.Ret.ReturnName}}, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + } + {{- else }} + if err != nil { + return {{.Ret.ReturnName}}, xerrors.WithStackTrace(err) + } + {{- end}} + return {{.Ret.ReturnName}}, nil +} +{{end}} + +{{if eq .Cmd ":many"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{if $.EmitMethodsWithDBArgument}}db DBTX, {{end}}{{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) ([]{{.Ret.DefineType}}, error) { + {{- $dbArg := "q.db" }}{{- if $.EmitMethodsWithDBArgument }}{{- $dbArg = "db" }}{{- end -}} + {{- if .Arg.IsEmpty }} + result, err := {{$dbArg}}.Query(ctx, {{.ConstantName}}, opts...) + {{- else }} + result, err := {{$dbArg}}.Query(ctx, {{.ConstantName}}, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ {{.Arg.YDBParamMapEntries}} })))..., + ) + {{- end }} + if err != nil { + {{- if $.WrapErrors}} + return nil, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return nil, xerrors.WithStackTrace(err) + {{- end }} + } + {{- if $.EmitEmptySlices}} + items := []{{.Ret.DefineType}}{} + {{else}} + var items []{{.Ret.DefineType}} + {{end -}} + for set, err := range result.ResultSets(ctx) { + if err != nil { + {{- if $.WrapErrors}} + return nil, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return nil, xerrors.WithStackTrace(err) + {{- end }} + } + for row, err := range set.Rows(ctx) { + if err != nil { + {{- if $.WrapErrors}} + return nil, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return nil, xerrors.WithStackTrace(err) + {{- end }} + } + var {{.Ret.Name}} {{.Ret.Type}} + if err := row.Scan({{.Ret.Scan}}); err != nil { + {{- if $.WrapErrors}} + return nil, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return nil, xerrors.WithStackTrace(err) + {{- end }} + } + items = append(items, {{.Ret.ReturnName}}) + } + } + if err := result.Close(ctx); err != nil { + {{- if $.WrapErrors}} + return nil, xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return nil, xerrors.WithStackTrace(err) + {{- end }} + } + return items, nil +} +{{end}} + +{{if eq .Cmd ":exec"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{if $.EmitMethodsWithDBArgument}}db DBTX, {{end}}{{if not .Arg.IsEmpty}}{{.Arg.Pair}}, {{end}}opts ...query.ExecuteOption) error { + {{- $dbArg := "q.db" }}{{- if $.EmitMethodsWithDBArgument }}{{- $dbArg = "db" }}{{- end -}} + {{- if .Arg.IsEmpty }} + err := {{$dbArg}}.Exec(ctx, {{.ConstantName}}, opts...) + {{- else }} + err := {{$dbArg}}.Exec(ctx, {{.ConstantName}}, + append(opts, query.WithParameters(ydb.ParamsFromMap(map[string]any{ {{.Arg.YDBParamMapEntries}} })))..., + ) + {{- end }} + if err != nil { + {{- if $.WrapErrors }} + return xerrors.WithStackTrace(fmt.Errorf("query {{.MethodName}}: %w", err)) + {{- else }} + return xerrors.WithStackTrace(err) + {{- end }} + } + return nil +} +{{end}} + +{{end}} +{{end}} +{{end}} diff --git a/internal/config/v_two.json b/internal/config/v_two.json index acf914997d..fd7084d6e8 100644 --- a/internal/config/v_two.json +++ b/internal/config/v_two.json @@ -38,7 +38,8 @@ "enum": [ "postgresql", "mysql", - "sqlite" + "sqlite", + "ydb" ] }, "schema": { diff --git a/internal/sqltest/local/ydb.go b/internal/sqltest/local/ydb.go index 8703b170b5..e3e51e3716 100644 --- a/internal/sqltest/local/ydb.go +++ b/internal/sqltest/local/ydb.go @@ -2,11 +2,8 @@ package local import ( "context" - "database/sql" "fmt" - "hash/fnv" "math/rand" - "net" "os" "testing" "time" @@ -14,104 +11,77 @@ import ( migrate "github.com/sqlc-dev/sqlc/internal/migrations" "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/query" ) func init() { rand.Seed(time.Now().UnixNano()) } -func YDB(t *testing.T, migrations []string) TestYDB { - return link_YDB(t, migrations, true) +func YDB(t *testing.T, migrations []string) *ydb.Driver { + return link_YDB(t, migrations, true, false) } -func ReadOnlyYDB(t *testing.T, migrations []string) TestYDB { - return link_YDB(t, migrations, false) +func YDBTLS(t *testing.T, migrations []string) *ydb.Driver { + return link_YDB(t, migrations, true, true) } -type TestYDB struct { - DB *sql.DB - Prefix string +func ReadOnlyYDB(t *testing.T, migrations []string) *ydb.Driver { + return link_YDB(t, migrations, false, false) } -func link_YDB(t *testing.T, migrations []string, rw bool) TestYDB { - t.Helper() - - time.Sleep(1 * time.Second) // wait for YDB to start +func ReadOnlyYDBTLS(t *testing.T, migrations []string) *ydb.Driver { + return link_YDB(t, migrations, false, true) +} +func link_YDB(t *testing.T, migrations []string, rw bool, tls bool) *ydb.Driver { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() + t.Helper() + dbuiri := os.Getenv("YDB_SERVER_URI") if dbuiri == "" { t.Skip("YDB_SERVER_URI is empty") } - host, _, err := net.SplitHostPort(dbuiri) - if err != nil { - t.Fatalf("invalid YDB_SERVER_URI: %q", dbuiri) - } baseDB := os.Getenv("YDB_DATABASE") if baseDB == "" { baseDB = "/local" } - var seed []string - files, err := sqlpath.Glob(migrations) - if err != nil { - t.Fatal(err) - } - h := fnv.New64() - for _, f := range files { - blob, err := os.ReadFile(f) - if err != nil { - t.Fatal(err) - } - h.Write(blob) - seed = append(seed, migrate.RemoveRollbackStatements(string(blob))) - } - - var name string - if rw { - // name = fmt.Sprintf("sqlc_test_%s", id()) - name = fmt.Sprintf("sqlc_test_%s", "test_new") + var connectionString string + if tls { + connectionString = fmt.Sprintf("grpcs://%s%s", dbuiri, baseDB) } else { - name = fmt.Sprintf("sqlc_test_%x", h.Sum(nil)) + connectionString = fmt.Sprintf("grpc://%s%s", dbuiri, baseDB) } - prefix := fmt.Sprintf("%s/%s", baseDB, name) - rootDSN := fmt.Sprintf("grpc://%s?database=%s", dbuiri, baseDB) - t.Logf("→ Opening root driver: %s", rootDSN) - driver, err := ydb.Open(ctx, rootDSN, + db, err := ydb.Open(ctx, connectionString, ydb.WithInsecure(), ydb.WithDiscoveryInterval(time.Hour), - ydb.WithNodeAddressMutator(func(_ string) string { - return host - }), ) if err != nil { - t.Fatalf("failed to open root YDB connection: %s", err) + t.Fatalf("failed to open YDB connection: %s", err) } - connector, err := ydb.Connector( - driver, - ydb.WithTablePathPrefix(prefix), - ydb.WithAutoDeclare(), - ydb.WithNumericArgs(), - ) + files, err := sqlpath.Glob(migrations) if err != nil { - t.Fatalf("failed to create connector: %s", err) + t.Fatal(err) } - db := sql.OpenDB(connector) - - t.Log("→ Applying migrations to prefix: ", prefix) + for _, f := range files { + blob, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + stmt := migrate.RemoveRollbackStatements(string(blob)) - schemeCtx := ydb.WithQueryMode(ctx, ydb.SchemeQueryMode) - for _, stmt := range seed { - _, err := db.ExecContext(schemeCtx, stmt) + err = db.Query().Exec(ctx, stmt, query.WithTxControl(query.EmptyTxControl())) if err != nil { t.Fatalf("failed to apply migration: %s\nSQL: %s", err, stmt) } } - return TestYDB{DB: db, Prefix: prefix} + + return db }