Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dev-1/lesson-3.1/go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

.PHONY: run
run:
go run .
19 changes: 19 additions & 0 deletions dev-1/lesson-3.1/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module ydb-sample

go 1.25.3

require github.com/ydb-platform/ydb-go-sdk/v3 v3.117.1

require (
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20250911135631-b3beddd517d9 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
176 changes: 176 additions & 0 deletions dev-1/lesson-3.1/go/go.sum

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions dev-1/lesson-3.1/go/issue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import (
"time"
)

type Issue struct {
Id int64 `sql:"id"`
Title string `sql:"title"`
Timestamp time.Time `sql:"created_at"`
}
159 changes: 159 additions & 0 deletions dev-1/lesson-3.1/go/issue_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package main

import (
"context"
"errors"
"math/rand"
"time"

ydb "github.com/ydb-platform/ydb-go-sdk/v3"
"github.com/ydb-platform/ydb-go-sdk/v3/query"
"github.com/ydb-platform/ydb-go-sdk/v3/sugar"
)

var (
random = rand.New(
rand.NewSource(time.Now().UnixNano()),
)
)

// Репозиторий для работы с тикетами в базе данных YDB
// Реализует операции добавления и чтения тикетов
type IssueRepository struct {
query *QueryHelper
}

func NewIssueRepository(query *QueryHelper) *IssueRepository {
return &IssueRepository{
query: query,
}
}

// Добавление нового тикета в БД
// ctx [context.Context] - контекст для управления исполнением запроса (например, можно задать таймаут)
// title [string] - название тикета
// Возвращает созданный тикет или ошибку
func (repo *IssueRepository) AddIssue(ctx context.Context, title string) (*Issue, error) {
// Генерируем случайный id для тикета
id := random.Int63() // do not repeat in production
timestamp := time.Now()

// Выполняем UPSERT запрос для добавления тикета
// Изменять данные можно только в режиме транзакции SERIALIZABLE_RW, поэтому используем его
err := repo.query.ExecuteTx(`
DECLARE $id AS Int64;
DECLARE $title AS Text;
DECLARE $created_at AS Timestamp;

UPSERT INTO issues (id, title, created_at)
VALUES ($id, $title, $created_at);
`,
ctx,
query.SerializableReadWriteTxControl(query.CommitTx()),
ydb.ParamsBuilder().
Param("$id").Int64(id).
Param("$title").Text(title).
Param("$created_at").Timestamp(timestamp).
Build(),
)
if err != nil {
return nil, err
}

return &Issue{
Id: id,
Title: title,
Timestamp: timestamp,
}, nil
}

// Возвращает тикет по заданному id
// ctx [context.Context] - контекст для управления исполнением запроса (например, можно задать таймаут)
// id [int64] - id тикета
// Возвращает найденный тикет или ошибку
func (repo *IssueRepository) FindById(ctx context.Context, id int64) (*Issue, error) {
result := make([]Issue, 0)

// Выполняем SELECT запрос в режиме [Snapshot Read-Only] для чтения данных
// Этот режим сообщает серверу, что эта транзакция только для чтения.
// Это позволяет снизить накладные расходы на подготовку к изменениям
// и просто читать данные из одного "слепка" базы данных.
repo.query.Query(`
SELECT
id,
title,
created_at
FROM issues
WHERE id=$id;
`,
ctx,
query.SnapshotReadOnlyTxControl(),
ydb.ParamsBuilder().
Param("$id").Int64(id).
Build(),
func(resultSet query.ResultSet, ctx context.Context) error {
for row, err := range sugar.UnmarshalRows[Issue](resultSet.Rows(ctx)) {
// Если во время чтения результата возникла ошибка,
// то необходимо очистить уже прочитанные результаты,
// чтобы избежать дублирования при следующем выполнении функции-ретраера
// и вернуть ретраеру ошибку
if err != nil {
clear(result)
return err
}

result = append(result, row)
}
return nil
},
)

if len(result) > 1 {
return nil, errors.New("Multiple rows with the same id (lol)")
}
if len(result) == 0 {
return nil, errors.New("Did not find any issues")
}
return &result[0], nil
}

// Получает все тикеты из базы данных
// ctx [context.Context] - контекст для управления исполнением запроса (например, можно задать таймаут)
func (repo *IssueRepository) FindAll(ctx context.Context) ([]Issue, error) {
result := make([]Issue, 0)

// Выполняем SELECT запрос в режиме [Snapshot Read-Only] для чтения данных
// Этот режим сообщает серверу, что эта транзакция только для чтения.
// Это позволяет снизить накладные расходы на подготовку к изменениям
// и просто читать данные из одного "слепка" базы данных.
err := repo.query.Query(`
SELECT
id,
title,
created_at
FROM issues;
`,
ctx,
query.SnapshotReadOnlyTxControl(),
ydb.ParamsBuilder().Build(),
func(resultSet query.ResultSet, ctx context.Context) error {
for row, err := range sugar.UnmarshalRows[Issue](resultSet.Rows(ctx)) {
// Если во время чтения результата возникла ошибка,
// то необходимо очистить уже прочитанные результаты,
// чтобы избежать дублирования при следующем выполнении функции-ретраера
// и вернуть ретраеру ошибку
if err != nil {
clear(result)
return err
}
result = append(result, row)
}
return nil
},
)
if err != nil {
return result, err
}

return result, nil
}
64 changes: 64 additions & 0 deletions dev-1/lesson-3.1/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"context"
"log"
"time"

"github.com/ydb-platform/ydb-go-sdk/v3"
)

// author: Egor Danilov
func main() {
connectionCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

dsn := "grpc://localhost:2136/local"

db, err := ydb.Open(connectionCtx, dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close(connectionCtx)

queryHelper := NewQueryHelper(db)

schemaRepository := NewSchemaRepository(queryHelper)
issuesRepository := NewIssueRepository(queryHelper)

queryCtx, queryCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer queryCancel()

schemaRepository.DropSchema(queryCtx)
schemaRepository.CreateSchema(queryCtx)

firstIssue, err := issuesRepository.AddIssue(queryCtx, "Ticket 1")
if err != nil {
log.Fatalf("Some error happened (1): %v\n", err)
}

_, err = issuesRepository.AddIssue(queryCtx, "Ticket 2")
if err != nil {
log.Fatalf("Some error happened (2): %v\n", err)
}

_, err = issuesRepository.AddIssue(queryCtx, "Ticket 3")
if err != nil {
log.Fatalf("Some error happened (3): %v\n", err)
}

issues, err := issuesRepository.FindAll(queryCtx)
if err != nil {
log.Fatalf("Some error happened while finding all: %v\n", err)
}
for _, issue := range issues {
log.Printf("Issue: %v\n", issue)
}

foundFirstIssue, err := issuesRepository.FindById(queryCtx, firstIssue.Id)
if err != nil {
log.Fatal(err)
} else {
log.Printf("First issue: %v\n", foundFirstIssue)
}
}
95 changes: 95 additions & 0 deletions dev-1/lesson-3.1/go/query_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"context"
"errors"
"io"

"github.com/ydb-platform/ydb-go-sdk/v3"
"github.com/ydb-platform/ydb-go-sdk/v3/query"
)

// Хелпер для выполнения запросов
// 1) Реализует Dependency Injection (принимает в функции-конструкторе указатель на драйвер).
// Это улучшает тестируемость - можно передать mock-объект для тестов
// 2) Универсальная логика выполнения запросов и чтения результатов
// 3) В методах принимает контекст, который может управлять исполнением запросов (например, через таймаут)
type QueryHelper struct {
driver *ydb.Driver
}

func NewQueryHelper(driver *ydb.Driver) *QueryHelper {
return &QueryHelper{
driver: driver,
}
}

func (q *QueryHelper) Execute(yql string, ctx context.Context) error {
return q.ExecuteTx(
yql,
ctx,
query.NoTx(),
ydb.ParamsBuilder().Build(),
)
}

func (q *QueryHelper) ExecuteTx(
yql string,
ctx context.Context,
txControl *query.TransactionControl,
params ydb.Params,
) error {
return q.driver.Query().Do(
ctx,
func(ctx context.Context, s query.Session) error {
err := s.Exec(
ctx,
yql,
query.WithTxControl(txControl),
query.WithParameters(params),
)
return err
},
)
}

func (q *QueryHelper) Query(
yql string,
ctx context.Context,
txControl *query.TransactionControl,
params ydb.Params,
materializeFunc func(query.ResultSet, context.Context) error,
) error {
return q.driver.Query().Do(
ctx,
func(ctx context.Context, s query.Session) error {
result, err := s.Query(
ctx,
yql,
query.WithTxControl(txControl),
query.WithParameters(params),
)

if err != nil {
return err
}

defer func() { _ = result.Close(ctx) }()

for {
resultSet, err := result.NextResultSet(ctx)
if err != nil {
if errors.Is(err, io.EOF) {
break
}

return err
}

materializeFunc(resultSet, ctx)
}

return nil
},
)
}
Loading