Skip to content

Commit d8d48dd

Browse files
author
serhii.zahuba
committed
FEATURE/queries-via-UI
1 parent a27f224 commit d8d48dd

File tree

6 files changed

+32
-45
lines changed

6 files changed

+32
-45
lines changed

backend/internal/features/query/postgresql/di.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@ var (
1212
)
1313

1414
func init() {
15-
// репозиторій та сервіс
15+
1616
sqlRepo = NewRepository()
1717
sqlService = NewService(sqlRepo)
1818

19-
// залежності з сусідніх модулів через їх DI
19+
2020
userSvc := users.GetUserService()
2121
dbSvc := databases.GetDatabaseService()
2222

23-
// контролер
23+
2424
sqlPgController = NewPostgresSqlQueryController(sqlService, userSvc, dbSvc)
2525
}
2626

2727
func GetSqlQueryRepository() *Repository { return sqlRepo }
2828
func GetSqlQueryService() *Service { return sqlService }
2929

30-
// Головне: цей контролер реєструємо у router
30+
3131
func GetPostgresSqlQueryController() *PostgresSqlQueryController {
3232
return sqlPgController
3333
}

backend/internal/features/query/postgresql/repository.go

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,24 @@ func (r *Repository) openPool(ctx context.Context, dsn string) (*pgxpool.Pool, e
2727
return pgxpool.NewWithConfig(ctx, cfg)
2828
}
2929

30-
func buildPostgresDSN(pg *postgresql.PostgresqlDatabase) (string, error) {
31-
if pg == nil {
30+
func buildPostgresDSN(p *postgresql.PostgresqlDatabase) (string, error) {
31+
if p == nil {
3232
return "", fmt.Errorf("postgres config is nil")
3333
}
34-
if pg.Database == nil {
35-
return "", fmt.Errorf("database name is nil")
36-
}
3734

38-
db := *pg.Database
35+
dbname := "postgres"
36+
if p.Database != nil && strings.TrimSpace(*p.Database) != "" {
37+
dbname = strings.TrimSpace(*p.Database)
38+
}
3939

40-
ssl := "disable"
41-
if pg.IsHttps {
42-
ssl = "require"
40+
sslMode := "disable"
41+
if p.IsHttps {
42+
sslMode = "require"
4343
}
4444

4545
return fmt.Sprintf(
4646
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
47-
pg.Host,
48-
pg.Port,
49-
pg.Username,
50-
pg.Password,
51-
db,
52-
ssl,
47+
p.Host, p.Port, p.Username, p.Password, dbname, sslMode,
5348
), nil
5449
}
5550

@@ -62,9 +57,7 @@ type Result struct {
6257
}
6358

6459
// ExecuteSelect — safe SELECT/CTE with forced LIMIT
65-
// ExecuteSQL — один стейтмент: SELECT/CTE виконуємо з лімітом, інші — через Exec
6660
func (r *Repository) ExecuteSQL(ctx context.Context, dbc *databases.Database, sql string, maxRows int) (*Result, error) {
67-
// підтримуємо лише PostgreSQL (як і раніше)
6861
t := strings.ToUpper(string(dbc.Type))
6962
if t != "POSTGRES" && t != "POSTGRESQL" {
7063
return nil, fmt.Errorf("only PostgreSQL type is supported, got %q", dbc.Type)
@@ -86,7 +79,7 @@ func (r *Repository) ExecuteSQL(ctx context.Context, dbc *databases.Database, sq
8679

8780
start := time.Now()
8881
if isSelect {
89-
// SELECT / WITH ... SELECT — обгортаємо лімітом
82+
9083
stmt = ensureLimit(stmt, maxRows)
9184

9285
rows, err := pool.Query(ctx, stmt)
@@ -120,37 +113,36 @@ func (r *Repository) ExecuteSQL(ctx context.Context, dbc *databases.Database, sq
120113
return &Result{
121114
Columns: cols,
122115
Rows: outRows,
123-
RowCount: rowCount, // кількість повернутих рядків
116+
RowCount: rowCount,
124117
Truncated: rowCount >= maxRows,
125118
Duration: time.Since(start),
126119
}, nil
127120
}
128121

129-
// DML/DDL: UPDATE/INSERT/DELETE/CREATE/... — просто виконуємо
122+
130123
tag, err := pool.Exec(ctx, stmt)
131124
if err != nil {
132125
return nil, err
133126
}
134127

135-
// фронту достатньо часу виконання; таблиця буде порожня
136-
// (за бажанням можна додати RowsAffected в DTO пізніше)
128+
137129
return &Result{
138130
Columns: []string{},
139131
Rows: [][]any{},
140-
RowCount: int(tag.RowsAffected()), // для інфи: скільки рядків зачеплено
132+
RowCount: int(tag.RowsAffected()),
141133
Truncated: false,
142134
Duration: time.Since(start),
143135
}, nil
144136
}
145137

146-
// дуже проста евристика: SELECT або WITH без DML-ключових слів
138+
147139
func isSelectLike(sql string) bool {
148140
up := strings.ToUpper(strings.TrimSpace(sql))
149141
if strings.HasPrefix(up, "SELECT ") {
150142
return true
151143
}
152144
if strings.HasPrefix(up, "WITH ") {
153-
// якщо це WITH ... INSERT/UPDATE/DELETE/... — вважаємо не-SELECT
145+
154146
for _, k := range []string{" INSERT ", " UPDATE ", " DELETE ", " CREATE ", " ALTER ", " DROP ", " TRUNCATE ", " GRANT ", " REVOKE "} {
155147
if strings.Contains(up, k) {
156148
return false

backend/internal/features/query/postgresql/service_test.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import (
1515
"github.com/stretchr/testify/assert"
1616
)
1717

18-
// ==== helpers (аналогічно до інших тестів у репо) ====
1918

20-
// беремо повну модель користувача для доступу до DatabaseService
19+
2120
func getTestUserModel(t *testing.T) *users_models.User {
2221
t.Helper()
2322

@@ -34,20 +33,19 @@ func getTestUserModel(t *testing.T) *users_models.User {
3433
return u
3534
}
3635

37-
// генеруємо унікальне ім'я тимчасової таблиці (не TEMP, бо різні конекшени)
36+
3837
func tmpTableName() string {
3938
return "tmp_sqlquery_" + strings.ReplaceAll(uuid.New().String(), "-", "_")
4039
}
4140

42-
// виконує один SQL через сервіс
41+
4342
func runSQL(t *testing.T, svc *Service, dbc *databases.Database, sql string) *ExecuteResponse {
4443
t.Helper()
4544
req := &ExecuteRequest{
46-
// DatabaseId тут не використовується сервісом, бо ми передаємо готовий dbc,
47-
// але залишимо для повноти
45+
4846
DatabaseID: dbc.ID,
4947
SQL: sql,
50-
// для SELECT сервіс сам підставить maxRows/timeout за замовчуванням
48+
5149
}
5250
resp, err := svc.Execute(dbc, req)
5351
assert.NoError(t, err, "execute failed for SQL: %s", sql)
@@ -57,7 +55,7 @@ func runSQL(t *testing.T, svc *Service, dbc *databases.Database, sql string) *Ex
5755
// ==== tests ====
5856

5957
func Test_Service_Execute_SelectUpdateDelete(t *testing.T) {
60-
// arrange: користувач, storage, notifier, database, svc, dbc
58+
6159
testUser := getTestUserModel(t)
6260

6361
testUserResp := users.GetTestUser()
@@ -76,7 +74,7 @@ func Test_Service_Execute_SelectUpdateDelete(t *testing.T) {
7674
assert.NoError(t, err)
7775
assert.NotNil(t, dbc)
7876

79-
svc := GetSqlQueryService() // через DI (internal/features/sqlquery/di.go)
77+
svc := GetSqlQueryService()
8078

8179
table := tmpTableName()
8280

@@ -104,27 +102,27 @@ func Test_Service_Execute_SelectUpdateDelete(t *testing.T) {
104102
assert.Equal(t, []string{"id", "name", "cnt"}, sel.Columns)
105103
assert.Len(t, sel.Rows, 3)
106104

107-
// прості перевірки значень (без жорсткого касту типів)
105+
// simple check
108106
row1 := sel.Rows[0]
109107
assert.Equal(t, "1", fmt.Sprint(row1[0]))
110108
assert.Equal(t, "a", fmt.Sprint(row1[1]))
111109
assert.Equal(t, "10", fmt.Sprint(row1[2]))
112110

113-
// 4) UPDATE двох рядків
111+
// 4) UPDATE two rows
114112
updateSQL := fmt.Sprintf(`UPDATE %s SET cnt = cnt + 5 WHERE id IN (1,3)`, table)
115113
upd := runSQL(t, svc, dbc, updateSQL)
116114
assert.Equal(t, 2, upd.RowCount)
117115
assert.Empty(t, upd.Columns)
118116
assert.Empty(t, upd.Rows)
119117

120-
// 5) DELETE одного рядка
118+
// 5) DELETE one rows
121119
deleteSQL := fmt.Sprintf(`DELETE FROM %s WHERE id = 2`, table)
122120
del := runSQL(t, svc, dbc, deleteSQL)
123121
assert.Equal(t, 1, del.RowCount)
124122
assert.Empty(t, del.Columns)
125123
assert.Empty(t, del.Rows)
126124

127-
// 6) повторний SELECT: лишилось 2 записи, cnt у 1й та 3й збільшився на 5
125+
// 6) second SELECT
128126
sel2 := runSQL(t, svc, dbc, selectSQL)
129127
assert.Equal(t, 2, sel2.RowCount)
130128
assert.Equal(t, []string{"id", "name", "cnt"}, sel2.Columns)

frontend/src/entity/query/api/sqlQueryApi.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/entity/api/sqlQueryApi.ts
21
import RequestOptions from '../../../shared/api/RequestOptions';
32
import { apiHelper } from '../../../shared/api/apiHelper';
43
import type { ExecuteResponse } from '../model/ExecuteResponse';

frontend/src/entity/query/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
// src/entity/query/index.ts
21
export * from './api/sqlQueryApi';
32
export * from './model/ExecuteResponse';
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
// src/features/sqlquery/index.ts
21
export { SqlQueryComponent } from './ui/SqlQueryComponent';

0 commit comments

Comments
 (0)