Skip to content

Commit

Permalink
WIP: DB List
Browse files Browse the repository at this point in the history
  • Loading branch information
jhrozek committed Jul 4, 2024
1 parent aa4e567 commit a69dec2
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 5 deletions.
21 changes: 21 additions & 0 deletions database/migrations/000072_profile_selectors_view.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Copyright 2024 Stacklok, Inc
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

BEGIN;

DROP VIEW IF EXISTS profiles_with_selectors;

DROP TYPE IF EXISTS selector_info;

COMMIT;
39 changes: 39 additions & 0 deletions database/migrations/000072_profile_selectors_view.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- Copyright 2024 Stacklok, Inc
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

BEGIN;

CREATE TYPE profile_selector AS (
id UUID,
profile_id UUID,
entity entities,
selector TEXT,
comment TEXT
);

CREATE VIEW profiles_with_selectors AS (
SELECT
p.id AS profid,
ARRAY_AGG(
ROW(ps.id, ps.profile_id, ps.entity, ps.selector, ps.comment)::profile_selector
) FILTER (WHERE ps.id IS NOT NULL) AS selectors
FROM
profiles p
LEFT JOIN
profile_selectors ps ON p.id = ps.profile_id
GROUP BY
p.id
);

COMMIT;
12 changes: 10 additions & 2 deletions database/query/profiles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,19 @@ SELECT * FROM profiles WHERE id = $1 AND project_id = $2 FOR UPDATE;
SELECT * FROM profiles WHERE lower(name) = lower(sqlc.arg(name)) AND project_id = $1 FOR UPDATE;

-- name: ListProfilesByProjectID :many
SELECT sqlc.embed(profiles), sqlc.embed(profiles_with_entity_profiles) FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid
SELECT
sqlc.embed(profiles),
sqlc.embed(profiles_with_entity_profiles),
sqlc.embed(profiles_with_selectors)
FROM profiles
JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid
JOIN profiles_with_selectors ON profiles.id = profiles_with_selectors.profid
WHERE profiles.project_id = $1;

-- name: ListProfilesByProjectIDAndLabel :many
SELECT sqlc.embed(profiles), sqlc.embed(profiles_with_entity_profiles) FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid
SELECT sqlc.embed(profiles), sqlc.embed(profiles_with_entity_profiles), sqlc.embed(profiles_with_selectors) FROM profiles
JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid
JOIN profiles_with_selectors ON profiles.id = profiles_with_selectors.profid
WHERE profiles.project_id = $1
AND (
-- the most common case first, if the include_labels is empty, we list profiles with no labels
Expand Down
50 changes: 50 additions & 0 deletions internal/db/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,62 @@
package db

import (
"fmt"
"slices"
"strings"

"github.com/sqlc-dev/pqtype"
)

func (s *ProfileSelector) Scan(value interface{}) error {

Check failure on line 25 in internal/db/domain.go

View workflow job for this annotation

GitHub Actions / lint / Run golangci-lint

exported: exported method ProfileSelector.Scan should have comment or be unexported (revive)
if value == nil {
return nil
}

// Convert the value to a string
bytes, ok := value.([]byte)
if !ok {
return fmt.Errorf("failed to scan SelectorInfo: %v", value)
}
str := string(bytes)
fmt.Println(str)

// Remove the parentheses
str = strings.TrimPrefix(str, "(")
str = strings.TrimSuffix(str, ")")

// Split the string by commas to get the individual field values
parts := strings.Split(str, ",")

// Assign the values to the struct fields
if len(parts) != 5 {
return fmt.Errorf("failed to scan SelectorInfo: unexpected number of fields")
}

if err := s.ID.Scan(parts[0]); err != nil {
return fmt.Errorf("failed to scan id: %v", err)
}

if err := s.ProfileID.Scan(parts[1]); err != nil {
return fmt.Errorf("failed to scan profile_id: %v", err)
}

s.Entity = NullEntities{}
if parts[2] != "" {
if err := s.Entity.Scan(parts[2]); err != nil {
return fmt.Errorf("failed to scan entity: %v", err)
}
}

selector := strings.TrimPrefix(parts[3], "\"")
selector = strings.TrimSuffix(selector, "\"")
s.Selector = selector

s.Comment = parts[4]

return nil
}

// This file contains domain-level methods for db structs

// CanImplement returns true if the provider implements the given type.
Expand Down
5 changes: 5 additions & 0 deletions internal/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions internal/db/profiles.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 122 additions & 0 deletions internal/db/profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ func createRandomProfile(t *testing.T, projectID uuid.UUID, labels []string) Pro
return prof
}

func createRepoSelector(t *testing.T, profileId uuid.UUID, sel string, comment string) ProfileSelector {

Check failure on line 53 in internal/db/profiles_test.go

View workflow job for this annotation

GitHub Actions / lint / Run golangci-lint

test helper function should start from t.Helper() (thelper)
return createEntitySelector(t, profileId, NullEntities{Entities: EntitiesRepository, Valid: true}, sel, comment)
}

func createEntitySelector(t *testing.T, profileId uuid.UUID, ent NullEntities, sel string, comment string) ProfileSelector {

Check failure on line 57 in internal/db/profiles_test.go

View workflow job for this annotation

GitHub Actions / lint / Run golangci-lint

test helper function should start from t.Helper() (thelper)
dbSel, err := testQueries.CreateSelector(context.Background(), CreateSelectorParams{
ProfileID: profileId,
Entity: ent,
Selector: sel,
Comment: comment,
})
require.NoError(t, err)
require.NotEmpty(t, dbSel)

return dbSel
}

func createRandomRuleType(t *testing.T, projectID uuid.UUID) RuleType {
t.Helper()

Expand Down Expand Up @@ -192,6 +209,111 @@ func createTestRandomEntities(t *testing.T) *testRandomEntities {
}
}

func matchIdWithListLabelRow(t *testing.T, id uuid.UUID) func(r ListProfilesByProjectIDAndLabelRow) bool {
t.Helper()

return func(r ListProfilesByProjectIDAndLabelRow) bool {
return r.Profile.ID == id
}
}

func matchIdWithListRow(t *testing.T, id uuid.UUID) func(r ListProfilesByProjectIDRow) bool {
t.Helper()

return func(r ListProfilesByProjectIDRow) bool {
return r.Profile.ID == id
}
}

func findRowWithLabels(t *testing.T, rows []ListProfilesByProjectIDAndLabelRow, id uuid.UUID) int {
t.Helper()

return slices.IndexFunc(rows, matchIdWithListLabelRow(t, id))
}

func findRow(t *testing.T, rows []ListProfilesByProjectIDRow, id uuid.UUID) int {
t.Helper()

return slices.IndexFunc(rows, matchIdWithListRow(t, id))
}

func TestProfileListWithSelectors(t *testing.T) {
t.Parallel()

randomEntities := createTestRandomEntities(t)

noSelectors := createRandomProfile(t, randomEntities.proj.ID, []string{})
oneSelectorProfile := createRandomProfile(t, randomEntities.proj.ID, []string{})
oneSel := createRepoSelector(t, oneSelectorProfile.ID, "one_selector1", "one_comment1")

multiSelectorProfile := createRandomProfile(t, randomEntities.proj.ID, []string{})
mulitSel1 := createRepoSelector(t, multiSelectorProfile.ID, "multi_selector1", "multi_comment1")
mulitSel2 := createRepoSelector(t, multiSelectorProfile.ID, "multi_selector2", "multi_comment2")
mulitSel3 := createRepoSelector(t, multiSelectorProfile.ID, "multi_selector3", "multi_comment3")

genericSelectorProfile := createRandomProfile(t, randomEntities.proj.ID, []string{})
genericSel := createEntitySelector(t, genericSelectorProfile.ID, NullEntities{}, "gen_selector1", "gen_comment1")

t.Run("list profiles with selectors using the label list", func(t *testing.T) {
t.Parallel()

rows, err := testQueries.ListProfilesByProjectIDAndLabel(
context.Background(), ListProfilesByProjectIDAndLabelParams{
ProjectID: randomEntities.proj.ID,
})
require.NoError(t, err)

require.Len(t, rows, 4)

noSelIdx := findRowWithLabels(t, rows, noSelectors.ID)
require.True(t, noSelIdx >= 0, "noSelectors not found in rows")
require.Empty(t, rows[noSelIdx].ProfilesWithSelector.Selectors)

oneSelIdx := findRowWithLabels(t, rows, oneSelectorProfile.ID)
require.True(t, oneSelIdx >= 0, "oneSelector not found in rows")
require.Len(t, rows[oneSelIdx].ProfilesWithSelector.Selectors, 1)
require.Contains(t, rows[oneSelIdx].ProfilesWithSelector.Selectors, oneSel)

multiSelIdx := findRowWithLabels(t, rows, multiSelectorProfile.ID)
require.True(t, multiSelIdx >= 0, "multiSelectorProfile not found in rows")
require.Len(t, rows[multiSelIdx].ProfilesWithSelector.Selectors, 3)
require.Subset(t, rows[multiSelIdx].ProfilesWithSelector.Selectors, []ProfileSelector{mulitSel1, mulitSel2, mulitSel3})

genSelIdx := findRowWithLabels(t, rows, genericSelectorProfile.ID)
require.Len(t, rows[genSelIdx].ProfilesWithSelector.Selectors, 1)
require.Contains(t, rows[genSelIdx].ProfilesWithSelector.Selectors, genericSel)
})

t.Run("list profiles with selectors using the non-label list", func(t *testing.T) {
t.Parallel()

rows, err := testQueries.ListProfilesByProjectID(
context.Background(), randomEntities.proj.ID)
require.NoError(t, err)

require.Len(t, rows, 4)

noSelIdx := findRow(t, rows, noSelectors.ID)
require.True(t, noSelIdx >= 0, "noSelectors not found in rows")
require.Empty(t, rows[noSelIdx].ProfilesWithSelector.Selectors)

oneSelIdx := findRow(t, rows, oneSelectorProfile.ID)
require.True(t, oneSelIdx >= 0, "oneSelector not found in rows")
require.Len(t, rows[oneSelIdx].ProfilesWithSelector.Selectors, 1)
require.Contains(t, rows[oneSelIdx].ProfilesWithSelector.Selectors, oneSel)

multiSelIdx := findRow(t, rows, multiSelectorProfile.ID)
require.True(t, multiSelIdx >= 0, "multiSelectorProfile not found in rows")
require.Len(t, rows[multiSelIdx].ProfilesWithSelector.Selectors, 3)
require.Subset(t, rows[multiSelIdx].ProfilesWithSelector.Selectors, []ProfileSelector{mulitSel1, mulitSel2, mulitSel3})

genSelIdx := findRow(t, rows, genericSelectorProfile.ID)
require.Len(t, rows[genSelIdx].ProfilesWithSelector.Selectors, 1)
require.Contains(t, rows[genSelIdx].ProfilesWithSelector.Selectors, genericSel)
})

}

func TestProfileLabels(t *testing.T) {
t.Parallel()

Expand Down
11 changes: 10 additions & 1 deletion sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ packages:
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
emit_empty_slices: true
emit_empty_slices: true
overrides:
- db_type: "uuid"
go_type:
import: "github.com/google/uuid"
type: "UUID"
- column: profiles_with_selectors.selectors
go_type:
type: "ProfileSelector"
slice: true

0 comments on commit a69dec2

Please sign in to comment.