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"`
}
227 changes: 227 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,227 @@
package main

import (
"context"
"errors"
"io"
"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 {
driver *ydb.Driver
}

func NewIssueRepository(driver *ydb.Driver) *IssueRepository {
return &IssueRepository{
driver: driver,
}
}

// Добавление нового тикета в БД
// 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.driver.Query().Do(
ctx,
func(ctx context.Context, s query.Session) error {
err := s.Exec(
ctx,
`
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);
`,
query.WithTxControl(
query.SerializableReadWriteTxControl(query.CommitTx()),
),
query.WithParameters(
ydb.ParamsBuilder().
Param("$id").Int64(id).
Param("$title").Text(title).
Param("$created_at").Timestamp(timestamp).
Build(),
),
)
return err
},
)
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) {
resultIssues := make([]Issue, 0)

// Выполняем SELECT запрос в режиме [Snapshot Read-Only] для чтения данных
// Этот режим сообщает серверу, что эта транзакция только для чтения.
// Это позволяет снизить накладные расходы на подготовку к изменениям
// и просто читать данные из одного "слепка" базы данных.
err := repo.driver.Query().Do(
ctx,
func(ctx context.Context, s query.Session) error {
// Если на предыдущих итерациях функции-ретраера
// возникла ошибка во время чтения результата,
// то необходимо очистить уже прочитанные результаты,
// чтобы избежать дублирования при следующем выполнении функции-ретраера
resultIssues = make([]Issue, 0)

queryResult, err := s.Query(
ctx,
`
SELECT
id,
title,
created_at
FROM issues
WHERE id=$id;
`,
query.WithTxControl(query.SnapshotReadOnlyTxControl()),
query.WithParameters(
ydb.ParamsBuilder().
Param("$id").Int64(id).
Build(),
),
)

if err != nil {
return err
}

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

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

return err
}

for row, err := range sugar.UnmarshalRows[Issue](resultSet.Rows(ctx)) {
if err != nil {
return err
}

resultIssues = append(resultIssues, row)
}
}

return nil
},
)
if err != nil {
return nil, err
}

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

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

// Выполняем SELECT запрос в режиме [Snapshot Read-Only] для чтения данных
// Этот режим сообщает серверу, что эта транзакция только для чтения.
// Это позволяет снизить накладные расходы на подготовку к изменениям
// и просто читать данные из одного "слепка" базы данных.
err := repo.driver.Query().Do(
ctx,
func(ctx context.Context, s query.Session) error {
// Если на предыдущих итерациях функции-ретраера
// возникла ошибка во время чтения результата,
// то необходимо очистить уже прочитанные результаты,
// чтобы избежать дублирования при следующем выполнении функции-ретраера
resultIssues = make([]Issue, 0)

queryResult, err := s.Query(
ctx,
`
SELECT
id,
title,
created_at
FROM issues;
`,
query.WithTxControl(query.SnapshotReadOnlyTxControl()),
query.WithParameters(
ydb.ParamsBuilder().Build(),
),
)

if err != nil {
return err
}

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

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

return err
}

for row, err := range sugar.UnmarshalRows[Issue](resultSet.Rows(ctx)) {
if err != nil {
return err
}

resultIssues = append(resultIssues, row)
}
}

return nil
},
)
if err != nil {
return resultIssues, err
}

return resultIssues, nil
}
62 changes: 62 additions & 0 deletions dev-1/lesson-3.1/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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)

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

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)
}
}
60 changes: 60 additions & 0 deletions dev-1/lesson-3.1/go/schema_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"log"

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

// Репозиторий для управления схемой базы данных YDB
// Отвечает за создание и удаление таблиц
type SchemaRepository struct {
driver *ydb.Driver
}

func NewSchemaRepository(driver *ydb.Driver) *SchemaRepository {
return &SchemaRepository{
driver: driver,
}
}

// Создает таблицу issues в базе данных
// Таблица содержит поля:
// - id: уникальный идентификатор тикета
// - title: название тикета
// - created_at: время создания тикета
// Все поля являются обязательными.
func (repo *SchemaRepository) CreateSchema(ctx context.Context) {
err := repo.driver.Query().Exec(
ctx,
`
CREATE TABLE IF NOT EXISTS issues (
id Int64 NOT NULL,
title Text NOT NULL,
created_at Timestamp NOT NULL,
PRIMARY KEY (id)
);
`,
query.WithTxControl(query.NoTx()),
query.WithParameters(ydb.ParamsBuilder().Build()),
)
if err != nil {
log.Fatal(err)
}
}

// Удаляет таблицу issues из базы данных
// Используется для очистки схемы перед созданием новой
func (repo *SchemaRepository) DropSchema(ctx context.Context) {
err := repo.driver.Query().Exec(
ctx,
"DROP TABLE IF EXISTS issues;",
query.WithTxControl(query.NoTx()),
query.WithParameters(ydb.ParamsBuilder().Build()),
)
if err != nil {
log.Fatal(err)
}
}