Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 29d41ab

Browse files
committed
Use sqlx for workspace build queries
1 parent a8ceb72 commit 29d41ab

File tree

10 files changed

+230
-14
lines changed

10 files changed

+230
-14
lines changed

coderd/autobuild/executor/lifecycle_executor_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package executor_test
22

33
import (
44
"context"
5-
"os"
65
"testing"
76
"time"
87

8+
"github.com/coder/coder/coderd/database/dbtestutil"
9+
910
"go.uber.org/goleak"
1011

1112
"github.com/google/uuid"
@@ -493,7 +494,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
493494
}
494495

495496
func TestExecutorAutostartMultipleOK(t *testing.T) {
496-
if os.Getenv("DB") == "" {
497+
if !dbtestutil.UsingRealDatabase() {
497498
t.Skip(`This test only really works when using a "real" database, similar to a HA setup`)
498499
}
499500

coderd/database/dbgen/generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat
231231
UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()),
232232
Roles: takeFirstSlice(orig.Roles, []string{}),
233233
})
234-
require.NoError(t, err, "insert organization")
234+
require.NoError(t, err, "insert organization member")
235235
return mem
236236
}
237237

coderd/database/dbgen/take.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ func takeFirstIP(values ...net.IPNet) net.IPNet {
1313
// takeFirstSlice implements takeFirst for []any.
1414
// []any is not a comparable type.
1515
func takeFirstSlice[T any](values ...[]T) []T {
16-
return takeFirstF(values, func(v []T) bool {
16+
out := takeFirstF(values, func(v []T) bool {
1717
return len(v) != 0
1818
})
19+
// Prevent nil slices
20+
if out == nil {
21+
return []T{}
22+
}
23+
return out
1924
}
2025

2126
// takeFirstF takes the first value that returns true

coderd/database/dbtestutil/db.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import (
1313
"github.com/coder/coder/coderd/database/postgres"
1414
)
1515

16+
func UsingRealDatabase() bool {
17+
return os.Getenv("DB") != ""
18+
}
19+
1620
func NewDB(t *testing.T) (database.Store, database.Pubsub) {
1721
t.Helper()
1822

1923
db := dbfake.New()
2024
pubsub := database.NewPubsubInMemory()
21-
if os.Getenv("DB") != "" {
25+
if UsingRealDatabase() {
2226
connectionURL := os.Getenv("CODER_PG_CONNECTION_URL")
2327
if connectionURL == "" {
2428
var (

coderd/database/modelqueries.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ type workspaceQuerier interface {
192192
type WorkspaceBuild struct {
193193
WorkspaceBuildThin
194194
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
195-
WorkspaceOwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
195+
WorkspaceOwnerID uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
196196
}
197197

198198
type getWorkspaceBuildParams struct {
@@ -203,6 +203,7 @@ type getWorkspaceBuildParams struct {
203203
BuildNumber int32 `db:"build_number"`
204204
LimitOpt int32 `db:"limit_opt"`
205205
Latest bool `db:"-"`
206+
TestLimit int `db:"-"`
206207
}
207208

208209
func (q *sqlQuerier) getWorkspaceBuild(ctx context.Context, arg getWorkspaceBuildParams) (WorkspaceBuild, error) {
@@ -233,7 +234,10 @@ type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct {
233234
}
234235

235236
func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) {
236-
return sqlxqueries.GetContext[WorkspaceBuild](ctx, q.sdb, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", arg)
237+
return q.getWorkspaceBuild(ctx, getWorkspaceBuildParams{
238+
BuildNumber: arg.BuildNumber,
239+
WorkspaceID: arg.WorkspaceID,
240+
})
237241
}
238242

239243
type GetWorkspaceBuildsByWorkspaceIDParams struct {

coderd/database/modelqueries_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package database_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/coderd/database"
12+
"github.com/coder/coder/coderd/database/dbgen"
13+
"github.com/coder/coder/coderd/database/dbtestutil"
14+
"github.com/coder/coder/coderd/rbac"
15+
)
16+
17+
func TestGetWorkspaceBuild(t *testing.T) {
18+
t.Parallel()
19+
if !dbtestutil.UsingRealDatabase() {
20+
t.Skip("Test only runs against a real database")
21+
}
22+
23+
db, _ := dbtestutil.NewDB(t)
24+
25+
// Seed the database with some workspace builds.
26+
var (
27+
org = dbgen.Organization(t, db, database.Organization{})
28+
user = dbgen.User(t, db, database.User{
29+
RBACRoles: []string{rbac.RoleOwner()},
30+
})
31+
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
32+
UserID: user.ID,
33+
OrganizationID: org.ID,
34+
})
35+
template = dbgen.Template(t, db, database.Template{
36+
OrganizationID: org.ID,
37+
CreatedBy: user.ID,
38+
})
39+
version = dbgen.TemplateVersion(t, db, database.TemplateVersion{
40+
TemplateID: uuid.NullUUID{
41+
UUID: template.ID,
42+
Valid: true,
43+
},
44+
OrganizationID: org.ID,
45+
CreatedBy: user.ID,
46+
})
47+
workspace = dbgen.Workspace(t, db, database.Workspace{
48+
OwnerID: user.ID,
49+
OrganizationID: org.ID,
50+
TemplateID: template.ID,
51+
})
52+
jobs = []database.ProvisionerJob{
53+
dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
54+
OrganizationID: org.ID,
55+
InitiatorID: user.ID,
56+
}),
57+
dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
58+
OrganizationID: org.ID,
59+
InitiatorID: user.ID,
60+
}),
61+
}
62+
builds = []database.WorkspaceBuildThin{
63+
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{
64+
WorkspaceID: workspace.ID,
65+
TemplateVersionID: version.ID,
66+
BuildNumber: 1,
67+
Transition: database.WorkspaceTransitionStart,
68+
InitiatorID: user.ID,
69+
JobID: jobs[0].ID,
70+
Reason: database.BuildReasonInitiator,
71+
CreatedAt: time.Now(),
72+
}),
73+
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuildThin{
74+
WorkspaceID: workspace.ID,
75+
TemplateVersionID: version.ID,
76+
BuildNumber: 2,
77+
Transition: database.WorkspaceTransitionStart,
78+
InitiatorID: user.ID,
79+
JobID: jobs[1].ID,
80+
Reason: database.BuildReasonInitiator,
81+
CreatedAt: time.Now().Add(time.Hour),
82+
}),
83+
}
84+
ctx = context.Background()
85+
)
86+
87+
t.Run("GetWorkspaceBuildByID", func(t *testing.T) {
88+
t.Parallel()
89+
for _, expected := range builds {
90+
build, err := db.GetWorkspaceBuildByID(ctx, expected.ID)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal")
95+
}
96+
})
97+
98+
t.Run("GetWorkspaceBuildByJobID", func(t *testing.T) {
99+
t.Parallel()
100+
for i, job := range jobs {
101+
build, err := db.GetWorkspaceBuildByJobID(ctx, job.ID)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
expected := builds[i]
106+
require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal")
107+
}
108+
})
109+
110+
t.Run("GetWorkspaceBuildsCreatedAfter", func(t *testing.T) {
111+
t.Parallel()
112+
builds, err := db.GetWorkspaceBuildsCreatedAfter(ctx, jobs[0].CreatedAt)
113+
if err != nil {
114+
t.Fatal(err)
115+
}
116+
expected := builds[1]
117+
require.Len(t, builds, 1, "should only be one build")
118+
require.Equal(t, expected, builds[0], "builds should be equal")
119+
})
120+
121+
t.Run("GetWorkspaceBuildByWorkspaceIDAndBuildNumber", func(t *testing.T) {
122+
t.Parallel()
123+
for _, expected := range builds {
124+
build, err := db.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
125+
BuildNumber: expected.BuildNumber,
126+
WorkspaceID: expected.WorkspaceID,
127+
})
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
require.Equal(t, expected, build.WorkspaceBuildThin, "builds should be equal")
132+
}
133+
})
134+
135+
t.Run("GetWorkspaceBuildsByWorkspaceID", func(t *testing.T) {
136+
t.Parallel()
137+
found, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
138+
WorkspaceID: workspace.ID,
139+
Since: builds[0].CreatedAt.Add(-1 * time.Hour),
140+
})
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
require.Len(t, found, 2, "should be two builds")
145+
exp := []database.WorkspaceBuildThin{
146+
builds[1],
147+
builds[0],
148+
}
149+
require.Equal(t, exp, toThins(found), "builds should be equal")
150+
})
151+
152+
t.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", func(t *testing.T) {
153+
t.Parallel()
154+
found, err := db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, []uuid.UUID{workspace.ID})
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
require.Len(t, found, 2, "should be two builds")
159+
require.Equal(t, builds, found, "builds should be equal")
160+
})
161+
162+
t.Run("GetLatestWorkspaceBuilds", func(t *testing.T) {
163+
t.Parallel()
164+
found, err := db.GetLatestWorkspaceBuilds(ctx)
165+
if err != nil {
166+
t.Fatal(err)
167+
}
168+
require.Len(t, found, 1, "should be only 1 build")
169+
require.Equal(t, builds[1], toThins(found), "builds should be equal")
170+
})
171+
172+
t.Run("GetLatestWorkspaceBuildByWorkspaceID", func(t *testing.T) {
173+
t.Parallel()
174+
found, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
175+
if err != nil {
176+
t.Fatal(err)
177+
}
178+
require.Equal(t, builds[1], found, "builds should be equal")
179+
})
180+
}
181+
182+
func toThins(builds []database.WorkspaceBuild) []database.WorkspaceBuildThin {
183+
thins := make([]database.WorkspaceBuildThin, len(builds))
184+
for i, build := range builds {
185+
thins[i] = build.WorkspaceBuildThin
186+
}
187+
return thins
188+
}

coderd/database/sqlxqueries/bindvars.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func bindNamed(query string, arg interface{}) (newQuery string, args []interface
3939
// Replace all names with the correct index
4040
for i, name := range names {
4141
rpl := fmt.Sprintf("$%d", i+1)
42+
if strings.Contains(query, rpl) {
43+
return "", nil,
44+
xerrors.Errorf("query contains both named params %q, and unnamed %q: choose one", name, rpl)
45+
}
4246
query = strings.ReplaceAll(query, name, rpl)
4347
// Remove the "@" prefix to match to the "db" struct tag.
4448
names[i] = strings.TrimPrefix(name, "@")

coderd/database/sqlxqueries/sqlx.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ func SelectContext[RT any](ctx context.Context, q sqlx.QueryerContext, queryName
1818
return empty, xerrors.Errorf("get query: %w", err)
1919
}
2020

21+
// No argument was given, use an empty struct.
22+
if argument == nil {
23+
argument = struct{}{}
24+
}
25+
2126
query, args, err := bindNamed(query, argument)
2227
if err != nil {
2328
return empty, xerrors.Errorf("bind named: %w", err)

coderd/database/sqlxqueries/sqlxqueries.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ var (
2121

2222
func queries() (*template.Template, error) {
2323
once.Do(func() {
24-
tpls, err := template.ParseFS(sqlxQueries, "*.gosql")
24+
tpls, err := template.New("").
25+
Funcs(template.FuncMap{
26+
"int32": func(i int) int32 { return int32(i) },
27+
}).ParseFS(sqlxQueries, "*.gosql")
2528
if err != nil {
2629
cachedError = xerrors.Errorf("developer error parse sqlx queries: %w", err)
30+
return
2731
}
2832
cached = tpls
2933
})

coderd/database/sqlxqueries/workspace.gosql

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ FROM
88
workspace_builds
99
INNER JOIN
1010
workspaces ON workspace_builds.workspace_id = workspaces.id
11-
);
12-
{{ end }}
11+
)
12+
{{ end }};
1313

1414

1515
{{ define "GetWorkspaceBuild" }}
16+
-- {{ . }}}}
1617
-- name: GetWorkspaceBuild :one
1718
SELECT
1819
*
1920
FROM
20-
{{ template "workspace_builds_rbac" }}
21+
{{ template "workspace_builds_rbac" }} workspace_builds
2122
WHERE
2223
CASE
2324
WHEN @build_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
@@ -53,7 +54,7 @@ WHERE
5354
ORDER BY
5455
build_number desc
5556
{{ end }}
56-
LIMIT @limit_opt;
57+
{{ if gt .TestLimit 0 }} LIMIT @limit_opt {{ end }};
5758
{{ end }}
5859

5960

@@ -62,9 +63,9 @@ LIMIT @limit_opt;
6263
SELECT
6364
*
6465
FROM
65-
{{ template "workspace_builds_rbac" }}
66+
{{ template "workspace_builds_rbac" }} workspace_builds
6667
WHERE
67-
workspace_builds.workspace_id = $1
68+
workspace_builds.workspace_id = @workspace_id
6869
AND workspace_builds.created_at > @since
6970
AND CASE
7071
-- This allows using the last element on a page as effectively a cursor.

0 commit comments

Comments
 (0)