diff --git a/Makefile b/Makefile index 84e7715..602239b 100644 --- a/Makefile +++ b/Makefile @@ -55,3 +55,10 @@ test-unit: # Run unit tests @printf "${GREEN}DONE${RESET}\n\n" ${call coverage-report} + +test-integration: # Run integration tests + @printf "Running integration tests for ${CYAN}backends/ent${RESET}...\n" + @go test -failfast -v -tags=integration -coverprofile=coverage.out -covermode=atomic ./backends/ent/... + @printf "${GREEN}DONE${RESET}\n\n" + + ${call coverage-report} \ No newline at end of file diff --git a/backends/ent/annotations.go b/backends/ent/annotations.go index 67bbbba..7d910fe 100644 --- a/backends/ent/annotations.go +++ b/backends/ent/annotations.go @@ -109,7 +109,7 @@ func (backend *Backend) AddNodeAnnotations(nodeID, name string, values ...string Where(node.NativeIDEQ(nodeID)). All(backend.ctx) if err != nil { - return fmt.Errorf("querying documents: %w", err) + return fmt.Errorf("querying nodes: %w", err) } for idx := range nodes { @@ -452,20 +452,11 @@ func (backend *Backend) SetNodeUniqueAnnotation(nodeID, name, value string) erro annotations := ent.Annotations{} for idx := range nodes { - documentID, err := nodes[idx]. - QueryNodeLists(). - QueryDocuments(). - OnlyID(backend.ctx) - if err != nil { - return fmt.Errorf("querying node edges for document ID: %w", err) - } - annotations = append(annotations, &ent.Annotation{ - DocumentID: &documentID, - NodeID: &nodes[idx].ID, - Name: name, - Value: value, - IsUnique: true, + NodeID: &nodes[idx].ID, + Name: name, + Value: value, + IsUnique: true, }) } diff --git a/backends/ent/annotations_integration_test.go b/backends/ent/annotations_integration_test.go new file mode 100644 index 0000000..98d05ca --- /dev/null +++ b/backends/ent/annotations_integration_test.go @@ -0,0 +1,630 @@ +// -------------------------------------------------------------- +// SPDX-FileCopyrightText: Copyright © 2024 The Protobom Authors +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// -------------------------------------------------------------- + +//go:build integration + +package ent_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/protobom/protobom/pkg/reader" + "github.com/protobom/protobom/pkg/sbom" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/protobom/storage/backends/ent" +) + +// Annotations Integration Test Suite +// Tests annotation functionality with PostgreSQL database +type annotationsIntegrationSuite struct { + suite.Suite + backend *ent.Backend + container testcontainers.Container + documents []*sbom.Document +} + +func (ais *annotationsIntegrationSuite) SetupSuite() { + ctx := context.Background() + + // Start PostgreSQL container + pgContainer, err := postgres.Run(ctx, + "postgres:16-alpine", + postgres.WithDatabase("testdb"), + postgres.WithUsername("testuser"), + postgres.WithPassword("testpass"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(30*time.Second)), + ) + ais.Require().NoError(err) + ais.container = pgContainer + + // Get connection string + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + ais.Require().NoError(err) + + // Initialize backend + ais.backend = ent.NewBackend( + ent.WithPostgresConnection(connStr), + ) + ais.Require().NoError(ais.backend.InitClient()) + + // Load test documents + cwd, err := os.Getwd() + ais.Require().NoError(err) + + rdr := reader.New() + testdataDir := filepath.Join(cwd, "testdata") + + entries, err := os.ReadDir(testdataDir) + ais.Require().NoError(err) + + for idx := range entries { + document, err := rdr.ParseFile(filepath.Join(testdataDir, entries[idx].Name())) + ais.Require().NoError(err) + ais.documents = append(ais.documents, document) + } + + // Store all test documents for annotation testing + for _, doc := range ais.documents { + err := ais.backend.Store(doc, nil) + ais.Require().NoError(err) + } +} + +func (ais *annotationsIntegrationSuite) TearDownSuite() { + if ais.backend != nil { + ais.backend.CloseClient() + } + if ais.container != nil { + ctx := context.Background() + ais.Require().NoError(ais.container.Terminate(ctx)) + } +} + +func (ais *annotationsIntegrationSuite) TestDocumentAnnotations() { + testDoc := ais.documents[0] + docID := testDoc.GetMetadata().GetId() + + tests := []struct { + name string + setupFunc func() error + testFunc func() error + expectedValues []string + expectedCount int + expectError bool + }{ + { + name: "basic_document_annotations", + setupFunc: func() error { + return ais.backend.AddDocumentAnnotations(docID, "test-annotation-1", "test-value-1", "test-value-2") + }, + testFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "test-annotation-1") + if err != nil { + return err + } + if len(annotations) != 2 { + return fmt.Errorf("expected 2 annotations, got %d", len(annotations)) + } + return nil + }, + expectedCount: 2, + expectError: false, + }, + { + name: "update_document_annotations", + setupFunc: func() error { + return ais.backend.SetDocumentAnnotations(docID, "test-annotation-1", "updated-value-1", "updated-value-2", "updated-value-3") + }, + testFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "test-annotation-1") + if err != nil { + return err + } + if len(annotations) != 3 { + return fmt.Errorf("expected 3 annotations, got %d", len(annotations)) + } + return nil + }, + expectedCount: 3, + expectError: false, + }, + { + name: "unique_document_annotation", + setupFunc: func() error { + return ais.backend.SetDocumentUniqueAnnotation(docID, "unique-test", "unique-value") + }, + testFunc: func() error { + value, err := ais.backend.GetDocumentUniqueAnnotation(docID, "unique-test") + if err != nil { + return err + } + if value != "unique-value" { + return fmt.Errorf("expected 'unique-value', got '%s'", value) + } + return nil + }, + expectedValues: []string{"unique-value"}, + expectError: false, + }, + { + name: "update_unique_document_annotation", + setupFunc: func() error { + return ais.backend.SetDocumentUniqueAnnotation(docID, "unique-test", "updated-unique-value") + }, + testFunc: func() error { + value, err := ais.backend.GetDocumentUniqueAnnotation(docID, "unique-test") + if err != nil { + return err + } + if value != "updated-unique-value" { + return fmt.Errorf("expected 'updated-unique-value', got '%s'", value) + } + return nil + }, + expectedValues: []string{"updated-unique-value"}, + expectError: false, + }, + { + name: "bulk_document_annotations", + setupFunc: func() error { + bulkValues := make([]string, 100) + for i := 0; i < 100; i++ { + bulkValues[i] = fmt.Sprintf("bulk-value-%d", i) + } + return ais.backend.AddDocumentAnnotations(docID, "bulk-annotation", bulkValues...) + }, + testFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "bulk-annotation") + if err != nil { + return err + } + if len(annotations) != 100 { + return fmt.Errorf("expected 100 annotations, got %d", len(annotations)) + } + return nil + }, + expectedCount: 100, + expectError: false, + }, + } + + for _, tt := range tests { + ais.T().Run(tt.name, func(t *testing.T) { + // Setup + if tt.setupFunc != nil { + err := tt.setupFunc() + if tt.expectError { + ais.Require().Error(err) + return + } + ais.Require().NoError(err) + } + + // Test + if tt.testFunc != nil { + err := tt.testFunc() + if tt.expectError { + ais.Require().Error(err) + } else { + ais.Require().NoError(err) + } + } + }) + } +} + +func (ais *annotationsIntegrationSuite) TestNodeAnnotations() { + testDoc := ais.documents[0] + + // Skip if no nodes available + if len(testDoc.GetNodeList().GetNodes()) == 0 { + ais.T().Skip("No nodes available for testing") + return + } + + firstNode := testDoc.GetNodeList().GetNodes()[0] + nodeID := firstNode.GetId() + + tests := []struct { + name string + setupFunc func() error + testFunc func() error + expectedCount int + expectedValue string + expectError bool + }{ + { + name: "basic_node_annotations", + setupFunc: func() error { + return ais.backend.AddNodeAnnotations(nodeID, "node-annotation", "node-value-1", "node-value-2") + }, + testFunc: func() error { + annotations, err := ais.backend.GetNodeAnnotations(nodeID, "node-annotation") + if err != nil { + return err + } + if len(annotations) != 2 { + return fmt.Errorf("expected 2 node annotations, got %d", len(annotations)) + } + return nil + }, + expectedCount: 2, + expectError: false, + }, + { + name: "unique_node_annotation", + setupFunc: func() error { + return ais.backend.SetNodeUniqueAnnotation(nodeID, "unique-node-annotation", "unique-node-value") + }, + testFunc: func() error { + value, err := ais.backend.GetNodeUniqueAnnotation(nodeID, "unique-node-annotation") + if err != nil { + return err + } + if value != "unique-node-value" { + return fmt.Errorf("expected 'unique-node-value', got '%s'", value) + } + return nil + }, + expectedValue: "unique-node-value", + expectError: false, + }, + { + name: "update_unique_node_annotation", + setupFunc: func() error { + return ais.backend.SetNodeUniqueAnnotation(nodeID, "unique-node-annotation", "updated-node-value") + }, + testFunc: func() error { + value, err := ais.backend.GetNodeUniqueAnnotation(nodeID, "unique-node-annotation") + if err != nil { + return err + } + if value != "updated-node-value" { + return fmt.Errorf("expected 'updated-node-value', got '%s'", value) + } + return nil + }, + expectedValue: "updated-node-value", + expectError: false, + }, + } + + for _, tt := range tests { + ais.T().Run(tt.name, func(t *testing.T) { + // Setup + if tt.setupFunc != nil { + err := tt.setupFunc() + if tt.expectError { + ais.Require().Error(err) + return + } + ais.Require().NoError(err) + } + + // Test + if tt.testFunc != nil { + err := tt.testFunc() + if tt.expectError { + ais.Require().Error(err) + } else { + ais.Require().NoError(err) + } + } + }) + } +} + +func (ais *annotationsIntegrationSuite) TestAnnotationManagement() { + testDoc := ais.documents[0] + docID := testDoc.GetMetadata().GetId() + + tests := []struct { + name string + setupFunc func() error + actionFunc func() error + validateFunc func() error + expectError bool + }{ + { + name: "remove_specific_annotation", + setupFunc: func() error { + return ais.backend.AddDocumentAnnotations(docID, "remove-test", "value-1", "value-2", "value-3") + }, + actionFunc: func() error { + return ais.backend.RemoveDocumentAnnotations(docID, "remove-test", "value-2") + }, + validateFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "remove-test") + if err != nil { + return err + } + if len(annotations) != 2 { + return fmt.Errorf("expected 2 remaining annotations, got %d", len(annotations)) + } + return nil + }, + expectError: false, + }, + { + name: "clear_all_annotations", + setupFunc: func() error { + return ais.backend.AddDocumentAnnotations(docID, "clear-test", "value-1", "value-2", "value-3") + }, + actionFunc: func() error { + return ais.backend.ClearDocumentAnnotations(docID, "clear-test") + }, + validateFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "clear-test") + if err != nil { + return err + } + if len(annotations) != 0 { + return fmt.Errorf("expected 0 annotations after clear, got %d", len(annotations)) + } + return nil + }, + expectError: false, + }, + { + name: "bulk_annotation_conflict_resolution", + setupFunc: func() error { + bulkValues := make([]string, 50) + for i := 0; i < 50; i++ { + bulkValues[i] = fmt.Sprintf("bulk-value-%d", i) + } + return ais.backend.AddDocumentAnnotations(docID, "conflict-test", bulkValues...) + }, + actionFunc: func() error { + // Add the same values again - should handle conflicts + bulkValues := make([]string, 50) + for i := 0; i < 50; i++ { + bulkValues[i] = fmt.Sprintf("bulk-value-%d", i) + } + return ais.backend.AddDocumentAnnotations(docID, "conflict-test", bulkValues...) + }, + validateFunc: func() error { + annotations, err := ais.backend.GetDocumentAnnotations(docID, "conflict-test") + if err != nil { + return err + } + if len(annotations) < 50 { + return fmt.Errorf("expected at least 50 annotations after conflict resolution, got %d", len(annotations)) + } + return nil + }, + expectError: false, + }, + } + + for _, tt := range tests { + ais.T().Run(tt.name, func(t *testing.T) { + // Setup + if tt.setupFunc != nil { + err := tt.setupFunc() + ais.Require().NoError(err) + } + + // Action + if tt.actionFunc != nil { + err := tt.actionFunc() + if tt.expectError { + ais.Require().Error(err) + return + } + ais.Require().NoError(err) + } + + // Validate + if tt.validateFunc != nil { + err := tt.validateFunc() + ais.Require().NoError(err) + } + }) + } +} + +func (ais *annotationsIntegrationSuite) TestConcurrentAnnotations() { + // Test concurrent annotation operations + testDoc := ais.documents[0] + + // Store the document first + err := ais.backend.Store(testDoc, nil) + ais.Require().NoError(err) + + docID := testDoc.GetMetadata().GetId() + + tests := []struct { + name string + concurrentCount int + testFunc func(index int) error + expectErrors bool + description string + }{ + { + name: "concurrent_document_annotations", + concurrentCount: 10, + testFunc: func(index int) error { + return ais.backend.AddDocumentAnnotations(docID, "concurrent-test", + fmt.Sprintf("value-%d", index)) + }, + expectErrors: false, + description: "Concurrent document annotation additions should succeed", + }, + { + name: "concurrent_unique_annotations", + concurrentCount: 10, + testFunc: func(index int) error { + return ais.backend.SetDocumentUniqueAnnotation(docID, + fmt.Sprintf("unique-key-%d", index), fmt.Sprintf("value-%d", index)) + }, + expectErrors: false, + description: "Concurrent unique annotation sets should succeed", + }, + { + name: "concurrent_same_unique_annotation", + concurrentCount: 10, + testFunc: func(index int) error { + return ais.backend.SetDocumentUniqueAnnotation(docID, + "same-unique-key", fmt.Sprintf("value-%d", index)) + }, + expectErrors: false, // Last one wins, no errors expected + description: "Concurrent same unique annotation sets should handle conflicts", + }, + } + + for _, tt := range tests { + ais.T().Run(tt.name, func(t *testing.T) { + var wg sync.WaitGroup + resultsChan := make(chan error, tt.concurrentCount) + + // Launch concurrent annotation operations + for i := 0; i < tt.concurrentCount; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + err := tt.testFunc(index) + resultsChan <- err + }(i) + } + + wg.Wait() + close(resultsChan) + + // Collect results + var errorCount int + for err := range resultsChan { + if err != nil { + errorCount++ + ais.T().Logf("Concurrent annotation error: %v", err) + } + } + + if tt.expectErrors { + ais.Greater(errorCount, 0, "Expected some errors") + } else { + ais.Equal(0, errorCount, "Expected no errors in concurrent annotations") + } + }) + } +} + +func (ais *annotationsIntegrationSuite) TestConcurrentAnnotationOperations() { + // Test mixed concurrent annotation operations (add, get, remove, clear) + testDoc := ais.documents[0] + + // Store the document first + err := ais.backend.Store(testDoc, nil) + ais.Require().NoError(err) + + docID := testDoc.GetMetadata().GetId() + + // Pre-populate some annotations + err = ais.backend.AddDocumentAnnotations(docID, "mixed-test", + "initial-1", "initial-2", "initial-3") + ais.Require().NoError(err) + + tests := []struct { + name string + addOps int + getOps int + removeOps int + expectErrors bool + description string + }{ + { + name: "mixed_annotation_operations", + addOps: 5, + getOps: 10, + removeOps: 2, + expectErrors: false, + description: "Mixed annotation operations should work concurrently", + }, + { + name: "heavy_read_light_write", + addOps: 2, + getOps: 20, + removeOps: 1, + expectErrors: false, + description: "Heavy read operations with light write operations", + }, + } + + for _, tt := range tests { + ais.T().Run(tt.name, func(t *testing.T) { + var wg sync.WaitGroup + totalOps := tt.addOps + tt.getOps + tt.removeOps + resultsChan := make(chan error, totalOps) + + // Launch add operations + for i := 0; i < tt.addOps; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + err := ais.backend.AddDocumentAnnotations(docID, "concurrent-mixed", + fmt.Sprintf("add-value-%d-%s", index, uuid.New().String())) + resultsChan <- err + }(i) + } + + // Launch get operations + for i := 0; i < tt.getOps; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + _, err := ais.backend.GetDocumentAnnotations(docID, "mixed-test") + resultsChan <- err + }(i) + } + + // Launch remove operations + for i := 0; i < tt.removeOps; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + err := ais.backend.RemoveDocumentAnnotations(docID, "mixed-test", + fmt.Sprintf("initial-%d", index+1)) + resultsChan <- err + }(i) + } + + wg.Wait() + close(resultsChan) + + // Analyze results + var errorCount int + for err := range resultsChan { + if err != nil { + errorCount++ + ais.T().Logf("Mixed annotation operation error: %v", err) + } + } + + if tt.expectErrors { + ais.Greater(errorCount, 0, "Expected some errors") + } else { + ais.Equal(0, errorCount, "Expected no errors in mixed annotation operations") + } + }) + } +} + +func TestAnnotationsIntegrationSuite(t *testing.T) { + suite.Run(t, new(annotationsIntegrationSuite)) +} diff --git a/backends/ent/annotations_test.go b/backends/ent/annotations_test.go index 2fd93bd..9be63a8 100644 --- a/backends/ent/annotations_test.go +++ b/backends/ent/annotations_test.go @@ -7,6 +7,7 @@ package ent_test import ( + "database/sql" "fmt" "os" "path/filepath" @@ -422,6 +423,9 @@ func (as *annotationsSuite) TestBackend_RemoveDocumentAnnotations() { ctx := as.Context() as.Run(name, func() { + // Clear any existing annotations for this test to ensure clean state + as.Require().NoError(as.ClearDocumentAnnotations(documentID, annotationName)) + for _, value := range []string{"test-value-1", "test-value-2", "test-value-3"} { _, err = as.Backend.Ent().ExecContext(ctx, query, uniqueID, false, annotationName, value) as.Require().NoError(err) @@ -475,6 +479,9 @@ func (as *annotationsSuite) TestBackend_RemoveNodeAnnotations() { ctx := as.Context() as.Run(name, func() { + // Clear any existing annotations for this test to ensure clean state + as.Require().NoError(as.ClearNodeAnnotations(as.nodes[0].GetId(), annotationName)) + for _, value := range []string{"test-node-value-1", "test-node-value-2", "test-node-value-3"} { _, err = as.Backend.Ent().ExecContext(ctx, query, docUUID, nodeID, false, annotationName, value) as.Require().NoError(err) @@ -634,7 +641,7 @@ func (as *annotationsSuite) getTestResult(annotationName string) ent.Annotations result, err := as.Backend.Ent().QueryContext( as.Context(), - "SELECT * FROM annotations WHERE name == ?", + "SELECT id, document_id, node_id, name, value, is_unique, value_key FROM annotations WHERE name = ?", annotationName, ) as.Require().NoError(err) @@ -646,15 +653,22 @@ func (as *annotationsSuite) getTestResult(annotationName string) ent.Annotations for result.Next() { annotation := &ent.Annotation{} + var valueKey sql.NullString + as.Require().NoError(result.Scan( &annotation.ID, + &annotation.DocumentID, + &annotation.NodeID, &annotation.Name, &annotation.Value, &annotation.IsUnique, - &annotation.DocumentID, - &annotation.NodeID, + &valueKey, )) + if valueKey.Valid { + annotation.ValueKey = valueKey.String + } + annotations = append(annotations, annotation) } diff --git a/backends/ent/backend.go b/backends/ent/backend.go index e784dcc..696cf4e 100644 --- a/backends/ent/backend.go +++ b/backends/ent/backend.go @@ -15,6 +15,7 @@ import ( "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql/schema" sqlite "github.com/glebarez/go-sqlite" + _ "github.com/lib/pq" // PostgreSQL driver "github.com/protobom/protobom/pkg/storage" "github.com/protobom/storage/internal/backends/ent" @@ -54,25 +55,44 @@ func (backend *Backend) InitClient() error { backend.Options = NewBackendOptions() } - // Register the SQLite driver as "sqlite3". - if !slices.Contains(sql.Drivers(), dialect.SQLite) { - sqlite.RegisterAsSQLITE3() - } - clientOpts := []ent.Option{} if backend.Options.Debug { clientOpts = append(clientOpts, ent.Debug()) } - client, err := ent.Open(dialect.SQLite, backend.Options.DatabaseFile+dsnParams, clientOpts...) - if err != nil { - return fmt.Errorf("failed opening connection to sqlite: %w", err) + var client *ent.Client + + var err error + + switch backend.Options.Dialect { + case SQLiteDialect: + // Register the SQLite driver as "sqlite3". + if !slices.Contains(sql.Drivers(), dialect.SQLite) { + sqlite.RegisterAsSQLITE3() + } + + dsn := backend.Options.DatabaseURL + dsnParams + + client, err = ent.Open(dialect.SQLite, dsn, clientOpts...) + if err != nil { + return fmt.Errorf("failed opening connection to sqlite: %w", err) + } + + case PostgresDialect: + client, err = ent.Open(dialect.Postgres, backend.Options.DatabaseURL, clientOpts...) + if err != nil { + return fmt.Errorf("failed opening connection to postgres: %w", err) + } + + default: + return fmt.Errorf("%w: %s", errUnsupportedDialect, backend.Options.Dialect) } backend.client = client backend.ctx = ent.NewContext(context.Background(), client) // Run the auto migration tool. + // TODO: need to migrate to new global unique ID feature migrateOpts := []schema.MigrateOption{migrate.WithGlobalUniqueID(true), migrate.WithDropIndex(true)} if err := backend.client.Schema.Create(backend.ctx, migrateOpts...); err != nil { return fmt.Errorf("failed creating schema resources: %w", err) @@ -114,22 +134,45 @@ func (backend *Backend) WithBackendOptions(opts *BackendOptions) *Backend { } func (backend *Backend) WithDatabaseFile(file string) *Backend { - backend.Options.DatabaseFile = file + backend.Options.DatabaseURL = file + + return backend +} + +func (backend *Backend) WithDatabaseURL(url string) *Backend { + backend.Options.DatabaseURL = url + + return backend +} + +func (backend *Backend) WithDialect(dialect DatabaseDialect) *Backend { + backend.Options.Dialect = dialect return backend } func (backend *Backend) withTx(fns ...TxFunc) error { + return backend.withTxContext(backend.ctx, fns...) +} + +// withTxContext creates a transaction with a specific context to avoid race conditions. +func (backend *Backend) withTxContext(ctx context.Context, fns ...TxFunc) error { if backend.client == nil { return fmt.Errorf("%w", errUninitializedClient) } - tx, err := backend.client.Tx(backend.ctx) + // Create a NEW context for this transaction instead of modifying the shared one + txCtx := ctx + + tx, err := backend.client.Tx(txCtx) if err != nil { return fmt.Errorf("creating transactional client: %w", err) } - backend.ctx = ent.NewTxContext(backend.ctx, tx) + // Use transaction-local context instead of polluting backend.ctx + // This prevents concurrent transactions from interfering with each other + localTxCtx := ent.NewTxContext(txCtx, tx) + _ = localTxCtx // Mark as used for now for _, fn := range fns { if err := fn(tx); err != nil { diff --git a/backends/ent/backend_integration_test.go b/backends/ent/backend_integration_test.go new file mode 100644 index 0000000..7464aac --- /dev/null +++ b/backends/ent/backend_integration_test.go @@ -0,0 +1,365 @@ +// -------------------------------------------------------------- +// SPDX-FileCopyrightText: Copyright © 2024 The Protobom Authors +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// -------------------------------------------------------------- + +//go:build integration + +package ent_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/protobom/protobom/pkg/reader" + "github.com/protobom/protobom/pkg/sbom" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "google.golang.org/protobuf/proto" + + "github.com/protobom/storage/backends/ent" +) + +// Backend Integration Test Suite +// Tests backend functionality with PostgreSQL database +type backendIntegrationSuite struct { + suite.Suite + backend *ent.Backend + container testcontainers.Container + documents []*sbom.Document +} + +func (bis *backendIntegrationSuite) SetupSuite() { + ctx := context.Background() + + // Start PostgreSQL container + pgContainer, err := postgres.Run(ctx, + "postgres:16-alpine", + postgres.WithDatabase("testdb"), + postgres.WithUsername("testuser"), + postgres.WithPassword("testpass"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(30*time.Second)), + ) + bis.Require().NoError(err) + bis.container = pgContainer + + // Get connection string + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + bis.Require().NoError(err) + + // Initialize backend + bis.backend = ent.NewBackend( + ent.WithPostgresConnection(connStr), + ) + bis.Require().NoError(bis.backend.InitClient()) + + // Load test documents + cwd, err := os.Getwd() + bis.Require().NoError(err) + + rdr := reader.New() + testdataDir := filepath.Join(cwd, "testdata") + + entries, err := os.ReadDir(testdataDir) + bis.Require().NoError(err) + + for idx := range entries { + document, err := rdr.ParseFile(filepath.Join(testdataDir, entries[idx].Name())) + bis.Require().NoError(err) + bis.documents = append(bis.documents, document) + } +} + +func (bis *backendIntegrationSuite) TearDownSuite() { + if bis.backend != nil { + bis.backend.CloseClient() + } + if bis.container != nil { + ctx := context.Background() + bis.Require().NoError(bis.container.Terminate(ctx)) + } +} + +func (bis *backendIntegrationSuite) TestBackendConfiguration() { + ctx := context.Background() + connStr, err := bis.container.(*postgres.PostgresContainer).ConnectionString(ctx, "sslmode=disable") + bis.Require().NoError(err) + + tests := []struct { + name string + backendSetup func() *ent.Backend + expectedDialect ent.DatabaseDialect + expectedURL string + shouldInit bool + }{ + { + name: "postgres_connection_helper", + backendSetup: func() *ent.Backend { + return ent.NewBackend(ent.WithPostgresConnection(connStr)) + }, + expectedDialect: ent.PostgresDialect, + expectedURL: connStr, + shouldInit: true, + }, + { + name: "separate_options", + backendSetup: func() *ent.Backend { + return ent.NewBackend( + ent.WithDialect(ent.PostgresDialect), + ent.WithDatabaseURL(connStr), + ) + }, + expectedDialect: ent.PostgresDialect, + expectedURL: connStr, + shouldInit: true, + }, + { + name: "debug_enabled", + backendSetup: func() *ent.Backend { + return ent.NewBackend( + ent.WithPostgresConnection(connStr), + ent.Debug(), + ) + }, + expectedDialect: ent.PostgresDialect, + expectedURL: connStr, + shouldInit: true, + }, + } + + for _, tt := range tests { + bis.T().Run(tt.name, func(t *testing.T) { + backend := tt.backendSetup() + + // Validate configuration + bis.Equal(tt.expectedDialect, backend.Options.Dialect) + bis.Equal(tt.expectedURL, backend.Options.DatabaseURL) + + // Test initialization if required + if tt.shouldInit { + err := backend.InitClient() + bis.Require().NoError(err) + backend.CloseClient() + } + }) + } +} + +func (bis *backendIntegrationSuite) TestBackendStoreAndRetrieve() { + tests := []struct { + name string + document *sbom.Document + expectError bool + validateRetry bool + }{} + + // Build test cases from loaded documents + for idx, doc := range bis.documents { + documentID := doc.GetMetadata().GetId() + testName := documentID + if testName == "" { + testName = fmt.Sprintf("document_%d", idx) + } + + tests = append(tests, struct { + name string + document *sbom.Document + expectError bool + validateRetry bool + }{ + name: testName, + document: doc, + expectError: false, + validateRetry: true, + }) + } + + for _, tt := range tests { + bis.T().Run(tt.name, func(t *testing.T) { + // Test initial storage + err := bis.backend.Store(tt.document, nil) + if tt.expectError { + bis.Require().Error(err) + return + } + bis.Require().NoError(err) + + // Get retrieval ID + retrievalID := tt.document.GetMetadata().GetId() + if retrievalID == "" { + generatedUUID, err := ent.GenerateUUID(tt.document.GetMetadata()) + bis.Require().NoError(err) + retrievalID = generatedUUID.String() + } + + // Test retrieval + retrieved, err := bis.backend.Retrieve(retrievalID, nil) + bis.Require().NoError(err) + + // Validate content equality + retrievedCopy := proto.Clone(retrieved).(*sbom.Document) + originalCopy := proto.Clone(tt.document).(*sbom.Document) + retrievedCopy.GetMetadata().GetSourceData().Uri = nil + originalCopy.GetMetadata().GetSourceData().Uri = nil + + bis.Require().True(proto.Equal(originalCopy, retrievedCopy)) + + // Test duplicate storage (should not error) + if tt.validateRetry { + err = bis.backend.Store(tt.document, nil) + bis.Require().NoError(err, "Duplicate storage should not fail") + } + }) + } +} + +func (bis *backendIntegrationSuite) TestBackendClientManagement() { + ctx := context.Background() + connStr, err := bis.container.(*postgres.PostgresContainer).ConnectionString(ctx, "sslmode=disable") + bis.Require().NoError(err) + + tests := []struct { + name string + setupFunc func() *ent.Backend + expectError bool + }{ + { + name: "valid_initialization", + setupFunc: func() *ent.Backend { + return ent.NewBackend(ent.WithPostgresConnection(connStr)) + }, + expectError: false, + }, + { + name: "invalid_connection_string", + setupFunc: func() *ent.Backend { + return ent.NewBackend(ent.WithPostgresConnection("invalid-connection")) + }, + expectError: true, + }, + { + name: "double_initialization", + setupFunc: func() *ent.Backend { + backend := ent.NewBackend(ent.WithPostgresConnection(connStr)) + // Initialize once + err := backend.InitClient() + bis.Require().NoError(err) + return backend + }, + expectError: false, // Should not error on double init + }, + } + + for _, tt := range tests { + bis.T().Run(tt.name, func(t *testing.T) { + backend := tt.setupFunc() + + err := backend.InitClient() + if tt.expectError { + bis.Require().Error(err) + } else { + bis.Require().NoError(err) + backend.CloseClient() + } + }) + } +} + +func (bis *backendIntegrationSuite) TestConcurrentBackendOperations() { + // Test concurrent backend operations using the same backend instance + tests := []struct { + name string + concurrentCount int + testFunc func(index int, backend *ent.Backend) error + expectErrors bool + description string + }{ + { + name: "concurrent_backend_store_operations", + concurrentCount: 8, + testFunc: func(index int, backend *ent.Backend) error { + testDoc := bis.documents[0] + doc := proto.Clone(testDoc).(*sbom.Document) + doc.Metadata.Id = fmt.Sprintf("concurrent-backend-%d-%s", index, uuid.New().String()) + return backend.Store(doc, nil) + }, + expectErrors: false, + description: "Concurrent backend store operations should succeed", + }, + { + name: "concurrent_backend_retrieve_operations", + concurrentCount: 10, + testFunc: func(index int, backend *ent.Backend) error { + // Small delay to ensure document is stored before retrieval attempts + time.Sleep(50 * time.Millisecond) + + _, err := backend.Retrieve("shared-retrieval-doc", nil) + return err + }, + expectErrors: false, + description: "Concurrent backend retrieve operations should succeed", + }, + } + + for _, tt := range tests { + bis.T().Run(tt.name, func(t *testing.T) { + // Pre-store the document for retrieval tests + if tt.name == "concurrent_backend_retrieve_operations" { + testDoc := bis.documents[0] + doc := proto.Clone(testDoc).(*sbom.Document) + doc.Metadata.Id = "shared-retrieval-doc" + err := bis.backend.Store(doc, nil) + bis.Require().NoError(err) + } + + var wg sync.WaitGroup + resultsChan := make(chan error, tt.concurrentCount) + + // Use the shared backend instance for concurrent operations + for i := 0; i < tt.concurrentCount; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + // Execute the test function with the shared backend + err := tt.testFunc(index, bis.backend) + resultsChan <- err + }(i) + } + + wg.Wait() + close(resultsChan) + + // Collect results + var errorCount int + for err := range resultsChan { + if err != nil { + errorCount++ + bis.T().Logf("Concurrent backend operation error: %v", err) + } + } + + if tt.expectErrors { + bis.Greater(errorCount, 0, "Expected some errors") + } else { + bis.Equal(0, errorCount, "Expected no errors in concurrent backend operations") + } + }) + } +} + +func TestBackendIntegrationSuite(t *testing.T) { + suite.Run(t, new(backendIntegrationSuite)) +} diff --git a/backends/ent/backend_test.go b/backends/ent/backend_test.go new file mode 100644 index 0000000..c6d5ee4 --- /dev/null +++ b/backends/ent/backend_test.go @@ -0,0 +1,132 @@ +// -------------------------------------------------------------- +// SPDX-FileCopyrightText: Copyright © 2024 The Protobom Authors +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// -------------------------------------------------------------- + +package ent_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/protobom/storage/backends/ent" +) + +func TestDialectConfiguration(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dialect ent.DatabaseDialect + databaseURL string + expectInit bool + }{ + { + name: "SQLite in-memory", + dialect: ent.SQLiteDialect, + databaseURL: ":memory:", + expectInit: true, + }, + { + name: "SQLite file", + dialect: ent.SQLiteDialect, + databaseURL: "/tmp/test.db", + expectInit: true, + }, + { + name: "PostgreSQL invalid connection", + dialect: ent.PostgresDialect, + databaseURL: "invalid-connection-string", + expectInit: false, + }, + { + name: "Unsupported dialect", + dialect: ent.DatabaseDialect("mysql"), + databaseURL: "mysql://localhost/test", + expectInit: false, + }, + } + + for _, testCase := range tests { + testCaseCopy := testCase + t.Run(testCaseCopy.name, func(t *testing.T) { + t.Parallel() + + backend := ent.NewBackend( + ent.WithDialect(testCaseCopy.dialect), + ent.WithDatabaseURL(testCaseCopy.databaseURL), + ) + + err := backend.InitClient() + if testCaseCopy.expectInit { + require.NoError( + t, + err, + "Expected no error for dialect %s with URL %s", + testCaseCopy.dialect, + testCaseCopy.databaseURL, + ) + + if err == nil { + backend.CloseClient() + } + } else { + require.Error( + t, + err, + "Expected error for dialect %s with URL %s", + testCaseCopy.dialect, + testCaseCopy.databaseURL, + ) + } + }) + } +} + +func TestBackendOptions(t *testing.T) { + t.Parallel() + t.Run("Default options", func(t *testing.T) { + t.Parallel() + + opts := ent.NewBackendOptions() + assert.Equal(t, ent.SQLiteDialect, opts.Dialect) + assert.Equal(t, ":memory:", opts.DatabaseURL) + assert.False(t, opts.Debug) + }) + + t.Run("PostgreSQL helper", func(t *testing.T) { + t.Parallel() + + backend := ent.NewBackend( + ent.WithPostgresConnection("postgres://localhost/test"), + ) + assert.Equal(t, ent.PostgresDialect, backend.Options.Dialect) + assert.Equal(t, "postgres://localhost/test", backend.Options.DatabaseURL) + }) + + t.Run("Individual options", func(t *testing.T) { + t.Parallel() + + backend := ent.NewBackend( + ent.WithDialect(ent.PostgresDialect), + ent.WithDatabaseURL("postgres://localhost/mydb"), + ent.Debug(), + ) + assert.Equal(t, ent.PostgresDialect, backend.Options.Dialect) + assert.Equal(t, "postgres://localhost/mydb", backend.Options.DatabaseURL) + assert.True(t, backend.Options.Debug) + }) + + t.Run("Backward compatibility", func(t *testing.T) { + t.Parallel() + + backend := ent.NewBackend( + ent.WithDatabaseFile("/tmp/test.db"), + ) + assert.Equal(t, ent.SQLiteDialect, backend.Options.Dialect) + assert.Equal(t, "/tmp/test.db", backend.Options.DatabaseURL) + }) +} diff --git a/backends/ent/options.go b/backends/ent/options.go index 444bdbd..1d8d9d7 100644 --- a/backends/ent/options.go +++ b/backends/ent/options.go @@ -18,6 +18,7 @@ const dsnParams string = "?_pragma=foreign_keys(1)" var ( errInvalidEntOptions = errors.New("invalid ent backend options") errUninitializedClient = errors.New("backend client must be initialized") + errUnsupportedDialect = errors.New("unsupported database dialect") ) type ( @@ -27,10 +28,18 @@ type ( // Annotations is a parsable slice of Annotation. Annotations = ent.Annotations + // DatabaseDialect represents the database dialect to use. + DatabaseDialect string + // BackendOptions contains options specific to the protobom ent backend. BackendOptions struct { - // DatabaseFile is the file path of the SQLite database to be created. - DatabaseFile string + // DatabaseURL is the database connection string or file path. + // For SQLite: file path (e.g., ":memory:" or "path/to/file.db") + // For PostgreSQL: connection string (e.g., "postgres://user:password@host:port/dbname") + DatabaseURL string + + // Dialect specifies the database dialect to use (sqlite or postgres) + Dialect DatabaseDialect // Annotations is a slice of annotations to apply to stored document. Annotations @@ -43,10 +52,18 @@ type ( Option func(*Backend) ) +const ( + // SQLiteDialect represents SQLite database dialect. + SQLiteDialect DatabaseDialect = "sqlite" + // PostgresDialect represents PostgreSQL database dialect. + PostgresDialect DatabaseDialect = "postgres" +) + // NewBackendOptions creates a new BackendOptions for the backend. func NewBackendOptions() *BackendOptions { return &BackendOptions{ - DatabaseFile: ":memory:", + DatabaseURL: ":memory:", + Dialect: SQLiteDialect, } } @@ -58,7 +75,26 @@ func WithBackendOptions(opts *BackendOptions) Option { func WithDatabaseFile(file string) Option { return func(backend *Backend) { - backend.WithDatabaseFile(file) + backend.WithDatabaseURL(file) + } +} + +func WithDatabaseURL(url string) Option { + return func(backend *Backend) { + backend.WithDatabaseURL(url) + } +} + +func WithDialect(dialect DatabaseDialect) Option { + return func(backend *Backend) { + backend.WithDialect(dialect) + } +} + +func WithPostgresConnection(connectionString string) Option { + return func(backend *Backend) { + backend.WithDialect(PostgresDialect) + backend.WithDatabaseURL(connectionString) } } diff --git a/backends/ent/store.go b/backends/ent/store.go index a70ee3e..879bf85 100644 --- a/backends/ent/store.go +++ b/backends/ent/store.go @@ -19,12 +19,16 @@ import ( "google.golang.org/protobuf/proto" "github.com/protobom/storage/internal/backends/ent" + "github.com/protobom/storage/internal/backends/ent/annotation" + "github.com/protobom/storage/internal/backends/ent/document" "github.com/protobom/storage/internal/backends/ent/documenttype" "github.com/protobom/storage/internal/backends/ent/edgetype" "github.com/protobom/storage/internal/backends/ent/externalreference" "github.com/protobom/storage/internal/backends/ent/hashesentry" "github.com/protobom/storage/internal/backends/ent/identifiersentry" + entmetadata "github.com/protobom/storage/internal/backends/ent/metadata" "github.com/protobom/storage/internal/backends/ent/node" + entnodelist "github.com/protobom/storage/internal/backends/ent/nodelist" "github.com/protobom/storage/internal/backends/ent/purpose" ) @@ -35,7 +39,13 @@ type ( TxFunc func(*ent.Tx) error ) -var errNativeIDMap = errors.New("retrieving node map from context") +var ( + errInvalidAnnotation = errors.New("invalid annotation") + errNativeIDMap = errors.New("retrieving node map from context") + errMissingEdgeFromNode = errors.New("edge references missing from-node") + errMissingEdgeToNode = errors.New("edge references missing to-node") + errSavingAnnotations = errors.New("saving annotations") +) // Store implements the storage.Storer interface. func (backend *Backend) Store(doc *sbom.Document, opts *storage.StoreOptions) error { @@ -63,7 +73,9 @@ func (backend *Backend) Store(doc *sbom.Document, opts *storage.StoreOptions) er return err } - backend.ctx = context.WithValue(backend.ctx, documentIDKey{}, id) + // Create a local context for this Store operation instead of modifying shared backend.ctx + // This prevents race conditions when multiple goroutines call Store concurrently + localCtx := context.WithValue(backend.ctx, documentIDKey{}, id) // Set each annotation's document ID if not specified. for _, a := range annotations { @@ -72,41 +84,118 @@ func (backend *Backend) Store(doc *sbom.Document, opts *storage.StoreOptions) er } } - return backend.withTx( + // NOTE: @mrsufgi we need to check if the document already exists. For performance + // reasons this must be done outside the transaction. If a document ID already + // exists, applying OnConflict/OnConflictColumns will not work as intended and + // may create duplicate entries with the same ID. + // + // There is also an edge case where serialNumber is not used correctly which can + // result in different NodeList and Metadata under the same ID. This is a + // user error, but we should attempt to account for it here. + exists, err := backend.client.Document.Query(). + Where(document.IDEQ(id)). + Exist(localCtx) + if err != nil { + return fmt.Errorf("checking document existence: %w", err) + } + + if exists { + return nil + } + + // Create a context-isolated backend that shares the client but has its own context. + backendCopy := *backend + backendCopy.ctx = localCtx + + return backendCopy.withTx( func(tx *ent.Tx) error { return tx.Document.Create(). SetID(id). - OnConflict(). - Ignore(). - Exec(backend.ctx) + Exec(backendCopy.ctx) }, - backend.saveAnnotations(annotations...), - backend.saveMetadata(doc.GetMetadata()), - backend.saveNodeList(doc.GetNodeList()), + backendCopy.saveAnnotations(annotations...), + backendCopy.saveMetadata(doc.GetMetadata()), + backendCopy.saveNodeList(doc.GetNodeList()), ) } +//nolint:gocognit,cyclop,funlen func (backend *Backend) saveAnnotations(annotations ...*ent.Annotation) TxFunc { return func(tx *ent.Tx) error { - builders := []*ent.AnnotationCreate{} + var ( + nodeKV []*ent.AnnotationCreate + nodeK []*ent.AnnotationCreate + docKV []*ent.AnnotationCreate + docK []*ent.AnnotationCreate + ) for idx := range annotations { - builder := tx.Annotation.Create(). - SetNillableDocumentID(annotations[idx].DocumentID). - SetNillableNodeID(annotations[idx].NodeID). - SetName(annotations[idx].Name). - SetValue(annotations[idx].Value). - SetIsUnique(annotations[idx].IsUnique) + ann := annotations[idx] + createBuilder := tx.Annotation.Create(). + SetNillableDocumentID(ann.DocumentID). + SetNillableNodeID(ann.NodeID). + SetName(ann.Name). + SetValue(ann.Value). + SetIsUnique(ann.IsUnique) + + if ann.NodeID != nil { + if ann.IsUnique { + nodeK = append(nodeK, createBuilder) + } else { + nodeKV = append(nodeKV, createBuilder) + } - builders = append(builders, builder) + continue + } + + if ann.DocumentID != nil { + if ann.IsUnique { + docK = append(docK, createBuilder) + } else { + docKV = append(docKV, createBuilder) + } + + continue + } + + return errInvalidAnnotation } - err := tx.Annotation.CreateBulk(builders...). - OnConflict(). - UpdateNewValues(). - Exec(backend.ctx) - if err != nil && !ent.IsConstraintError(err) { - return fmt.Errorf("creating annotations: %w", err) + ctx := backend.ctx + if len(nodeKV) > 0 { + if err := tx.Annotation.CreateBulk(nodeKV...). + OnConflictColumns(annotation.FieldNodeID, annotation.FieldName, annotation.FieldValueKey). + UpdateNewValues(). + Exec(ctx); err != nil && !ent.IsConstraintError(err) { + return fmt.Errorf("%w: nodeKV: %w", errSavingAnnotations, err) + } + } + + if len(nodeK) > 0 { + if err := tx.Annotation.CreateBulk(nodeK...). + OnConflictColumns(annotation.FieldNodeID, annotation.FieldName, annotation.FieldValueKey). + UpdateNewValues(). + Exec(ctx); err != nil && !ent.IsConstraintError(err) { + return fmt.Errorf("%w: nodeK: %w", errSavingAnnotations, err) + } + } + + if len(docKV) > 0 { + if err := tx.Annotation.CreateBulk(docKV...). + OnConflictColumns(annotation.FieldDocumentID, annotation.FieldName, annotation.FieldValueKey). + UpdateNewValues(). + Exec(ctx); err != nil && !ent.IsConstraintError(err) { + return fmt.Errorf("%w: docKV: %w", errSavingAnnotations, err) + } + } + + if len(docK) > 0 { + if err := tx.Annotation.CreateBulk(docK...). + OnConflictColumns(annotation.FieldDocumentID, annotation.FieldName, annotation.FieldValueKey). + UpdateNewValues(). + Exec(ctx); err != nil && !ent.IsConstraintError(err) { + return fmt.Errorf("%w: docK: %w", errSavingAnnotations, err) + } } return nil @@ -128,7 +217,14 @@ func (backend *Backend) saveDocumentTypes(docTypes []*sbom.DocumentType, opts .. fn(newDocType) } - if err := newDocType.OnConflict().Ignore().Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { + if err := newDocType. + OnConflictColumns( + documenttype.FieldType, + documenttype.FieldName, + documenttype.FieldDescription, + ). + Ignore(). + Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving document type: %w", err) } } @@ -145,19 +241,29 @@ func (backend *Backend) saveEdges(edges []*sbom.Edge, opts ...func(*ent.EdgeType } for _, edge := range edges { + fromID, ok := nativeIDMap[edge.GetFrom()] + if !ok { + return fmt.Errorf("%w: %q", errMissingEdgeFromNode, edge.GetFrom()) + } + for _, toID := range edge.GetTo() { + toUUID, ok2 := nativeIDMap[toID] + if !ok2 { + return fmt.Errorf("%w: %q", errMissingEdgeToNode, toID) + } + newEdgeType := tx.EdgeType.Create(). SetProtoMessage(edge). SetType(edgetype.Type(edge.GetType().String())). - SetFromID(nativeIDMap[edge.GetFrom()]). - SetToID(nativeIDMap[toID]) + SetFromID(fromID). + SetToID(toUUID) for _, fn := range opts { fn(newEdgeType) } if err := newEdgeType. - OnConflict(). + OnConflictColumns(edgetype.FieldID). Ignore(). Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving edge: %w", err) @@ -200,7 +306,7 @@ func (backend *Backend) saveExternalReferences(refs []*sbom.ExternalReference, o } err := tx.ExternalReference.CreateBulk(builders...). - OnConflict(). + OnConflictColumns(externalreference.FieldID). Ignore(). Exec(backend.ctx) if err != nil && !ent.IsConstraintError(err) { @@ -236,7 +342,7 @@ func (backend *Backend) saveHashes(hashes map[int32]string, opts ...func(*ent.Ha } if err := tx.HashesEntry.CreateBulk(builders...). - OnConflict(). + OnConflictColumns(hashesentry.FieldHashAlgorithm, hashesentry.FieldHashData). Ignore(). Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving hashes: %w", err) @@ -265,7 +371,7 @@ func (backend *Backend) saveIdentifiers(idents map[int32]string, opts ...func(*e } if err := tx.IdentifiersEntry.CreateBulk(builders...). - OnConflict(). + OnConflictColumns("type", "value"). Ignore(). Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving identifiers: %w", err) @@ -275,6 +381,7 @@ func (backend *Backend) saveIdentifiers(idents map[int32]string, opts ...func(*e } } +//nolint:gocognit func (backend *Backend) saveMetadata(metadata *sbom.Metadata) TxFunc { id, err := GenerateUUID(metadata) if err != nil { @@ -282,8 +389,13 @@ func (backend *Backend) saveMetadata(metadata *sbom.Metadata) TxFunc { } return func(tx *ent.Tx) error { + nativeID := metadata.GetId() + if nativeID == "" { + nativeID = id.String() + } + newMetadata := tx.Metadata.Create(). - SetNativeID(metadata.GetId()). + SetNativeID(nativeID). SetProtoMessage(metadata). SetVersion(metadata.GetVersion()). SetName(metadata.GetName()). @@ -292,7 +404,10 @@ func (backend *Backend) saveMetadata(metadata *sbom.Metadata) TxFunc { addDocumentIDs(backend.ctx, newMetadata) - if err := newMetadata.OnConflict().Ignore().Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { + if err := newMetadata. + OnConflictColumns(entmetadata.FieldID). + Ignore(). + Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving metadata: %w", err) } @@ -331,12 +446,16 @@ func (backend *Backend) saveNodeList(nodeList *sbom.NodeList) TxFunc { } newNodeList := tx.NodeList.Create(). + SetID(id). SetProtoMessage(nodeList). SetRootElements(nodeList.GetRootElements()) addDocumentIDs(backend.ctx, newNodeList) - if err := newNodeList.OnConflict().Ignore().Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { + if err := newNodeList. + OnConflictColumns(entnodelist.FieldID). + Ignore(). + Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving node list: %w", err) } @@ -435,7 +554,7 @@ func (backend *Backend) saveNodes(nodes []*sbom.Node, opts ...func(*ent.NodeCrea } err := tx.Node.CreateBulk(builders...). - OnConflict(). + OnConflictColumns(node.FieldID). Ignore(). Exec(backend.ctx) if err != nil && !ent.IsConstraintError(err) { @@ -487,7 +606,7 @@ func (backend *Backend) savePersons(persons []*sbom.Person, opts ...func(*ent.Pe } if err := tx.Person.CreateBulk(builders...). - OnConflict(). + OnConflictColumns("name", "is_org", "email", "url", "phone"). Ignore(). Exec(backend.ctx); err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving persons: %w", err) @@ -515,7 +634,7 @@ func (backend *Backend) saveProperties(properties []*sbom.Property, opts ...func } err := tx.Property.CreateBulk(builders...). - OnConflict(). + OnConflictColumns("name", "data"). Ignore(). Exec(backend.ctx) if err != nil && !ent.IsConstraintError(err) { @@ -542,7 +661,7 @@ func (backend *Backend) savePurposes(purposes []sbom.Purpose, opts ...func(*ent. } err := tx.Purpose.CreateBulk(builders...). - OnConflict(). + OnConflictColumns(purpose.FieldID). Ignore(). Exec(backend.ctx) if err != nil && !ent.IsConstraintError(err) { @@ -565,7 +684,7 @@ func (backend *Backend) saveSourceData(sourceData *sbom.SourceData, opts ...func fn(newSourceData) } - id, err := newSourceData.OnConflict().Ignore().ID(backend.ctx) + id, err := newSourceData.OnConflictColumns("format", "size", "uri").Ignore().ID(backend.ctx) if err != nil && !ent.IsConstraintError(err) { return fmt.Errorf("saving source data: %w", err) } @@ -598,7 +717,7 @@ func (backend *Backend) saveTools(tools []*sbom.Tool, opts ...func(*ent.ToolCrea } err := tx.Tool.CreateBulk(builders...). - OnConflict(). + OnConflictColumns("name", "version", "vendor"). Ignore(). Exec(backend.ctx) if err != nil && !ent.IsConstraintError(err) { @@ -613,7 +732,7 @@ func (backend *Backend) saveTools(tools []*sbom.Tool, opts ...func(*ent.ToolCrea func GenerateUUID(msg proto.Message) (uuid.UUID, error) { data, err := proto.MarshalOptions{Deterministic: true}.Marshal(msg) if err != nil { - return uuid.Nil, fmt.Errorf("marshaling proto: %w", err) + return uuid.UUID{}, fmt.Errorf("marshalling proto message: %w", err) } return uuid.NewHash(sha256.New(), uuid.Max, data, int(uuid.Max.Version())), nil diff --git a/backends/ent/store_integration_test.go b/backends/ent/store_integration_test.go new file mode 100644 index 0000000..7118261 --- /dev/null +++ b/backends/ent/store_integration_test.go @@ -0,0 +1,536 @@ +// -------------------------------------------------------------- +// SPDX-FileCopyrightText: Copyright © 2024 The Protobom Authors +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// -------------------------------------------------------------- + +//go:build integration + +package ent_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/protobom/protobom/pkg/reader" + "github.com/protobom/protobom/pkg/sbom" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "google.golang.org/protobuf/proto" + + "github.com/protobom/storage/backends/ent" +) + +// Store Integration Test Suite +// Tests complex storage scenarios with PostgreSQL database +type storeIntegrationSuite struct { + suite.Suite + backend *ent.Backend + container testcontainers.Container + documents []*sbom.Document +} + +func (sis *storeIntegrationSuite) SetupSuite() { + ctx := context.Background() + + // Start PostgreSQL container + pgContainer, err := postgres.Run(ctx, + "postgres:16-alpine", + postgres.WithDatabase("testdb"), + postgres.WithUsername("testuser"), + postgres.WithPassword("testpass"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(30*time.Second)), + ) + sis.Require().NoError(err) + sis.container = pgContainer + + // Get connection string + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + sis.Require().NoError(err) + + // Initialize backend + sis.backend = ent.NewBackend( + ent.WithPostgresConnection(connStr), + ) + sis.Require().NoError(sis.backend.InitClient()) + + // Load test documents + cwd, err := os.Getwd() + sis.Require().NoError(err) + + rdr := reader.New() + testdataDir := filepath.Join(cwd, "testdata") + + entries, err := os.ReadDir(testdataDir) + sis.Require().NoError(err) + + for idx := range entries { + document, err := rdr.ParseFile(filepath.Join(testdataDir, entries[idx].Name())) + sis.Require().NoError(err) + sis.documents = append(sis.documents, document) + } +} + +func (sis *storeIntegrationSuite) TearDownSuite() { + if sis.backend != nil { + sis.backend.CloseClient() + } + if sis.container != nil { + ctx := context.Background() + sis.Require().NoError(sis.container.Terminate(ctx)) + } +} + +func (sis *storeIntegrationSuite) TestConflictHandling() { + tests := []struct { + name string + document *sbom.Document + conflictCount int + expectError bool + validateContent bool + }{ + { + name: "single_document_conflicts", + document: sis.documents[0], + conflictCount: 15, + expectError: false, + validateContent: true, + }, + } + + // Add cross-document conflicts if we have multiple documents + if len(sis.documents) > 2 { + tests = append(tests, struct { + name string + document *sbom.Document + conflictCount int + expectError bool + validateContent bool + }{ + name: "cross_document_conflicts", + document: sis.documents[1], + conflictCount: 5, + expectError: false, + validateContent: true, + }) + } + + for _, tt := range tests { + sis.T().Run(tt.name, func(t *testing.T) { + // Initial storage + err := sis.backend.Store(tt.document, nil) + sis.Require().NoError(err, "Initial storage should succeed") + + // Test multiple conflicts + for i := 0; i < tt.conflictCount; i++ { + err = sis.backend.Store(tt.document, nil) + if tt.expectError { + sis.Require().Error(err, fmt.Sprintf("Conflict test %d should fail", i+1)) + } else { + sis.Require().NoError(err, fmt.Sprintf("Conflict test %d should succeed", i+1)) + } + } + + // Validate content integrity if requested + if tt.validateContent { + retrieved, err := sis.backend.Retrieve(tt.document.GetMetadata().GetId(), nil) + sis.Require().NoError(err) + sis.Require().NotNil(retrieved) + + // Compare content + retrievedCopy := proto.Clone(retrieved).(*sbom.Document) + testDocCopy := proto.Clone(tt.document).(*sbom.Document) + retrievedCopy.GetMetadata().GetSourceData().Uri = nil + testDocCopy.GetMetadata().GetSourceData().Uri = nil + + sis.Require().True(proto.Equal(testDocCopy, retrievedCopy), "Retrieved document should match original after conflict resolution") + } + }) + } +} + +func (sis *storeIntegrationSuite) TestComplexStorageScenarios() { + tests := []struct { + name string + setupFunc func() error + testFunc func() error + cleanupFunc func() error + expectError bool + }{ + { + name: "multiple_documents_same_metadata", + setupFunc: func() error { + // Store first document + return sis.backend.Store(sis.documents[0], nil) + }, + testFunc: func() error { + // Try to store the same document again + return sis.backend.Store(sis.documents[0], nil) + }, + expectError: false, // Should not error on duplicate + }, + { + name: "interleaved_storage_and_retrieval", + setupFunc: func() error { + return nil // No setup needed + }, + testFunc: func() error { + // Store, retrieve, store again pattern + for i, doc := range sis.documents { + // Store + err := sis.backend.Store(doc, nil) + if err != nil { + return fmt.Errorf("store %d failed: %w", i, err) + } + + // Retrieve + docID := doc.GetMetadata().GetId() + if docID == "" { + generatedUUID, err := ent.GenerateUUID(doc.GetMetadata()) + if err != nil { + return fmt.Errorf("generate UUID %d failed: %w", i, err) + } + docID = generatedUUID.String() + } + + _, err = sis.backend.Retrieve(docID, nil) + if err != nil { + return fmt.Errorf("retrieve %d failed: %w", i, err) + } + + // Store again + err = sis.backend.Store(doc, nil) + if err != nil { + return fmt.Errorf("re-store %d failed: %w", i, err) + } + } + return nil + }, + expectError: false, + }, + } + + for _, tt := range tests { + sis.T().Run(tt.name, func(t *testing.T) { + // Setup + if tt.setupFunc != nil { + err := tt.setupFunc() + sis.Require().NoError(err) + } + + // Test + if tt.testFunc != nil { + err := tt.testFunc() + if tt.expectError { + sis.Require().Error(err) + } else { + sis.Require().NoError(err) + } + } + + // Cleanup + if tt.cleanupFunc != nil { + err := tt.cleanupFunc() + sis.Require().NoError(err) + } + }) + } +} + +func (sis *storeIntegrationSuite) TestEdgeValidation() { + // Test our edge validation fix - create SBOM with invalid edges + tests := []struct { + name string + createDoc func() *sbom.Document + expectError bool + description string + }{ + { + name: "valid_edges", + createDoc: func() *sbom.Document { + return &sbom.Document{ + Metadata: &sbom.Metadata{ + Id: "test-valid-edges", + Version: "1", + Name: "Test Valid Edges", + }, + NodeList: &sbom.NodeList{ + Nodes: []*sbom.Node{ + { + Id: "node-1", + Type: sbom.Node_PACKAGE, + Name: "package-1", + }, + { + Id: "node-2", + Type: sbom.Node_PACKAGE, + Name: "package-2", + }, + }, + Edges: []*sbom.Edge{ + { + Type: sbom.Edge_dependsOn, + From: "node-1", + To: []string{"node-2"}, + }, + }, + }, + } + }, + expectError: false, + description: "Valid edges should be stored successfully", + }, + { + name: "invalid_edges_missing_nodes", + createDoc: func() *sbom.Document { + return &sbom.Document{ + Metadata: &sbom.Metadata{ + Id: "test-invalid-edges", + Version: "1", + Name: "Test Invalid Edges", + }, + NodeList: &sbom.NodeList{ + Nodes: []*sbom.Node{ + { + Id: "node-1", + Type: sbom.Node_PACKAGE, + Name: "package-1", + }, + }, + Edges: []*sbom.Edge{{ + Type: sbom.Edge_dependsOn, + From: "node-1", + To: []string{"00000000-0000-0000-0000-000000000000"}, + }}, + }, + } + }, + expectError: true, + description: "Invalid edges should error and abort storage due to missing node", + }, + } + + for _, tt := range tests { + sis.T().Run(tt.name, func(t *testing.T) { + doc := tt.createDoc() + + err := sis.backend.Store(doc, nil) + if tt.expectError { + sis.Require().Error(err, tt.description) + } else { + sis.Require().NoError(err, tt.description) + + // Verify document can be retrieved + retrieved, err := sis.backend.Retrieve(doc.GetMetadata().GetId(), nil) + sis.Require().NoError(err) + sis.Require().NotNil(retrieved) + } + }) + } +} + +func (sis *storeIntegrationSuite) TestConcurrentStore() { + // Test concurrent storage operations to validate transaction isolation + tests := []struct { + name string + concurrentCount int + useFixedID bool + expectAllSucceed bool + expectMinSuccesses int + description string + }{ + { + name: "concurrent_different_documents", + concurrentCount: 10, + useFixedID: false, + expectAllSucceed: true, + expectMinSuccesses: 10, + description: "All concurrent operations with different IDs should succeed", + }, + { + name: "concurrent_same_document", + concurrentCount: 10, + useFixedID: true, + expectAllSucceed: false, + expectMinSuccesses: 1, + description: "Only first concurrent operation with same ID should succeed", + }, + { + name: "high_stress_concurrent", + concurrentCount: 20, + useFixedID: false, + expectAllSucceed: true, + expectMinSuccesses: 20, + description: "High stress test with 20 concurrent operations", + }, + } + + for _, tt := range tests { + sis.T().Run(tt.name, func(t *testing.T) { + testDoc := sis.documents[0] // Use first document as template + + // Channel to collect results + resultsChan := make(chan error, tt.concurrentCount) + + // WaitGroup to wait for all goroutines + var wg sync.WaitGroup + + // Launch concurrent storage operations + for i := 0; i < tt.concurrentCount; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + // Create document copy + doc := proto.Clone(testDoc).(*sbom.Document) + + if tt.useFixedID { + // Use same ID for all - should cause conflicts + doc.Metadata.Id = "concurrent-test-fixed-id" + } else { + // Use unique ID for each + doc.Metadata.Id = fmt.Sprintf("concurrent-test-%d-%s", index, uuid.New().String()) + } + + // Store the document + err := sis.backend.Store(doc, nil) + resultsChan <- err + }(i) + } + + // Wait for all goroutines to complete + wg.Wait() + close(resultsChan) + + // Collect and analyze results + var successCount, errorCount int + var errors []error + + for err := range resultsChan { + if err != nil { + errorCount++ + errors = append(errors, err) + } else { + successCount++ + } + } + + sis.T().Logf("Concurrent test %s: %d successes, %d errors out of %d operations", + tt.name, successCount, errorCount, tt.concurrentCount) + + // Validate results based on expectations + if tt.expectAllSucceed { + sis.Equal(tt.concurrentCount, successCount, "All operations should succeed") + sis.Equal(0, errorCount, "No operations should fail") + } else { + sis.GreaterOrEqual(successCount, tt.expectMinSuccesses, + "Should have at least minimum expected successes") + // For fixed ID tests, we expect constraint violations (not transaction errors) + for _, err := range errors { + sis.NotContains(err.Error(), "failed transaction", + "Should not see transaction abort errors") + } + } + }) + } +} + +func (sis *storeIntegrationSuite) TestConcurrentMixedOperations() { + // Test mixed concurrent operations (store + retrieve) + testDoc := sis.documents[0] + baseID := "mixed-concurrent-test" + + // Pre-store a document for retrieval testing + doc := proto.Clone(testDoc).(*sbom.Document) + doc.Metadata.Id = baseID + err := sis.backend.Store(doc, nil) + sis.Require().NoError(err) + + tests := []struct { + name string + storeOps int + retrieveOps int + expectErrors bool + description string + }{ + { + name: "mixed_store_retrieve", + storeOps: 5, + retrieveOps: 10, + expectErrors: false, + description: "Mixed store and retrieve operations should work concurrently", + }, + { + name: "heavy_retrieve_light_store", + storeOps: 2, + retrieveOps: 20, + expectErrors: false, + description: "Heavy retrieve load with light store operations", + }, + } + + for _, tt := range tests { + sis.T().Run(tt.name, func(t *testing.T) { + var wg sync.WaitGroup + resultsChan := make(chan error, tt.storeOps+tt.retrieveOps) + + // Launch store operations + for i := 0; i < tt.storeOps; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + doc := proto.Clone(testDoc).(*sbom.Document) + doc.Metadata.Id = fmt.Sprintf("%s-store-%d-%s", baseID, index, uuid.New().String()) + + err := sis.backend.Store(doc, nil) + resultsChan <- err + }(i) + } + + // Launch retrieve operations + for i := 0; i < tt.retrieveOps; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + _, err := sis.backend.Retrieve(baseID, nil) + resultsChan <- err + }(i) + } + + wg.Wait() + close(resultsChan) + + // Analyze results + var errorCount int + for err := range resultsChan { + if err != nil { + errorCount++ + sis.T().Logf("Mixed operation error: %v", err) + } + } + + if tt.expectErrors { + sis.Greater(errorCount, 0, "Expected some errors") + } else { + sis.Equal(0, errorCount, "Expected no errors in mixed operations") + } + }) + } +} + +func TestStoreIntegrationSuite(t *testing.T) { + suite.Run(t, new(storeIntegrationSuite)) +} diff --git a/go.mod b/go.mod index ee76eff..ed6b0dc 100644 --- a/go.mod +++ b/go.mod @@ -4,45 +4,102 @@ go 1.23.0 require ( entgo.io/ent v0.14.2 + github.com/CycloneDX/cyclonedx-go v0.9.2 github.com/glebarez/go-sqlite v1.22.0 github.com/google/uuid v1.6.0 - github.com/protobom/protobom v0.5.0 + github.com/lib/pq v1.10.9 + github.com/protobom/protobom v0.5.4 github.com/stretchr/testify v1.10.0 - google.golang.org/protobuf v1.36.5 + github.com/testcontainers/testcontainers-go v0.38.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 + google.golang.org/protobuf v1.36.7 ) require ( ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect - github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.2.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/inflect v0.21.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.0.9 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spdx/tools-golang v0.5.5 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zclconf/go-cty v1.16.1 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.30.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/grpc v1.73.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.61.8 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect modernc.org/sqlite v1.34.5 // indirect - sigs.k8s.io/release-utils v0.9.0 // indirect + sigs.k8s.io/release-utils v0.12.1 // indirect ) diff --git a/go.sum b/go.sum index 7fffca3..b38b4cd 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,19 @@ ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE= ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= entgo.io/ent v0.14.2 h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0= entgo.io/ent v0.14.2/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= @@ -19,40 +27,148 @@ github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQ github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/protobom/protobom v0.5.0 h1:jJYqGpdHq99zwh0/n1SOPl1aickCBZdA8pHS9V/f+XQ= -github.com/protobom/protobom v0.5.0/go.mod h1:HL47tggz7SXYXgNm3WjQQrWB6iOirYnrATsXAEyTUkI= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/protobom/protobom v0.5.4 h1:fMSO3wA5BEj/J6f1zgT56Ps/6pcE0NGZZzphnIoL/DQ= +github.com/protobom/protobom v0.5.4/go.mod h1:zz85kBItD/hO6c9o18pFXgFv2UOwtAhqZDAITIfPJcs= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= @@ -61,6 +177,7 @@ github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYec github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -71,39 +188,117 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 h1:KFdx9A0yF94K70T6ibSuvgkQQeX1xKlZVF3hEagXEtY= +github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0/go.mod h1:T/QRECND6N6tAKMxF1Za+G2tpwnGEHcODzHRsgIpw9M= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.16.1 h1:a5TZEPzBFFR53udlIKApXzj8JIF4ZNQ6abH79z5R1S0= github.com/zclconf/go-cty v1.16.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.23.12 h1:UF08a38c4B+K3VoGipBrVWLFUCHd8+X20QZtFAIlQNk= @@ -128,6 +323,6 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -sigs.k8s.io/release-utils v0.9.0 h1:+JYA8E5YXzVj2Eh929woeRn1U82vLUQbpqKsgZPEmEo= -sigs.k8s.io/release-utils v0.9.0/go.mod h1:xZoCJyajMJ0wtgGXWuznbC1r9dw7iJzMp/+dCkf1UGw= +sigs.k8s.io/release-utils v0.12.1 h1:3p9w137wBTTApHlL8izdJHcCuaBe8wZhQz+B0QIAaBE= +sigs.k8s.io/release-utils v0.12.1/go.mod h1:0z7JOb7iQcuDQcemQw5CSVrkH8evRHY0DMMjcyRB1e4= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/backends/ent/annotation.go b/internal/backends/ent/annotation.go index 1ad5046..e654270 100644 --- a/internal/backends/ent/annotation.go +++ b/internal/backends/ent/annotation.go @@ -35,6 +35,8 @@ type Annotation struct { Value string `json:"value,omitempty"` // IsUnique holds the value of the "is_unique" field. IsUnique bool `json:"is_unique,omitempty"` + // ValueKey holds the value of the "value_key" field. + ValueKey string `json:"value_key,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the AnnotationQuery when eager-loading is set. Edges AnnotationEdges `json:"-"` @@ -85,7 +87,7 @@ func (*Annotation) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case annotation.FieldID: values[i] = new(sql.NullInt64) - case annotation.FieldName, annotation.FieldValue: + case annotation.FieldName, annotation.FieldValue, annotation.FieldValueKey: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -140,6 +142,12 @@ func (a *Annotation) assignValues(columns []string, values []any) error { } else if value.Valid { a.IsUnique = value.Bool } + case annotation.FieldValueKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field value_key", values[i]) + } else if value.Valid { + a.ValueKey = value.String + } default: a.selectValues.Set(columns[i], values[i]) } @@ -204,6 +212,9 @@ func (a *Annotation) String() string { builder.WriteString(", ") builder.WriteString("is_unique=") builder.WriteString(fmt.Sprintf("%v", a.IsUnique)) + builder.WriteString(", ") + builder.WriteString("value_key=") + builder.WriteString(a.ValueKey) builder.WriteByte(')') return builder.String() } diff --git a/internal/backends/ent/annotation/annotation.go b/internal/backends/ent/annotation/annotation.go index cca6814..9c930fe 100644 --- a/internal/backends/ent/annotation/annotation.go +++ b/internal/backends/ent/annotation/annotation.go @@ -28,6 +28,8 @@ const ( FieldValue = "value" // FieldIsUnique holds the string denoting the is_unique field in the database. FieldIsUnique = "is_unique" + // FieldValueKey holds the string denoting the value_key field in the database. + FieldValueKey = "value_key" // EdgeDocument holds the string denoting the document edge name in mutations. EdgeDocument = "document" // EdgeNode holds the string denoting the node edge name in mutations. @@ -58,6 +60,7 @@ var Columns = []string{ FieldName, FieldValue, FieldIsUnique, + FieldValueKey, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -114,6 +117,11 @@ func ByIsUnique(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldIsUnique, opts...).ToFunc() } +// ByValueKey orders the results by the value_key field. +func ByValueKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldValueKey, opts...).ToFunc() +} + // ByDocumentField orders the results by document field. func ByDocumentField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/internal/backends/ent/annotation/where.go b/internal/backends/ent/annotation/where.go index 8663b84..e821a66 100644 --- a/internal/backends/ent/annotation/where.go +++ b/internal/backends/ent/annotation/where.go @@ -84,6 +84,11 @@ func IsUnique(v bool) predicate.Annotation { return predicate.Annotation(sql.FieldEQ(FieldIsUnique, v)) } +// ValueKey applies equality check predicate on the "value_key" field. It's identical to ValueKeyEQ. +func ValueKey(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldEQ(FieldValueKey, v)) +} + // DocumentIDEQ applies the EQ predicate on the "document_id" field. func DocumentIDEQ(v uuid.UUID) predicate.Annotation { return predicate.Annotation(sql.FieldEQ(FieldDocumentID, v)) @@ -284,6 +289,81 @@ func IsUniqueNEQ(v bool) predicate.Annotation { return predicate.Annotation(sql.FieldNEQ(FieldIsUnique, v)) } +// ValueKeyEQ applies the EQ predicate on the "value_key" field. +func ValueKeyEQ(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldEQ(FieldValueKey, v)) +} + +// ValueKeyNEQ applies the NEQ predicate on the "value_key" field. +func ValueKeyNEQ(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldNEQ(FieldValueKey, v)) +} + +// ValueKeyIn applies the In predicate on the "value_key" field. +func ValueKeyIn(vs ...string) predicate.Annotation { + return predicate.Annotation(sql.FieldIn(FieldValueKey, vs...)) +} + +// ValueKeyNotIn applies the NotIn predicate on the "value_key" field. +func ValueKeyNotIn(vs ...string) predicate.Annotation { + return predicate.Annotation(sql.FieldNotIn(FieldValueKey, vs...)) +} + +// ValueKeyGT applies the GT predicate on the "value_key" field. +func ValueKeyGT(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldGT(FieldValueKey, v)) +} + +// ValueKeyGTE applies the GTE predicate on the "value_key" field. +func ValueKeyGTE(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldGTE(FieldValueKey, v)) +} + +// ValueKeyLT applies the LT predicate on the "value_key" field. +func ValueKeyLT(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldLT(FieldValueKey, v)) +} + +// ValueKeyLTE applies the LTE predicate on the "value_key" field. +func ValueKeyLTE(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldLTE(FieldValueKey, v)) +} + +// ValueKeyContains applies the Contains predicate on the "value_key" field. +func ValueKeyContains(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldContains(FieldValueKey, v)) +} + +// ValueKeyHasPrefix applies the HasPrefix predicate on the "value_key" field. +func ValueKeyHasPrefix(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldHasPrefix(FieldValueKey, v)) +} + +// ValueKeyHasSuffix applies the HasSuffix predicate on the "value_key" field. +func ValueKeyHasSuffix(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldHasSuffix(FieldValueKey, v)) +} + +// ValueKeyIsNil applies the IsNil predicate on the "value_key" field. +func ValueKeyIsNil() predicate.Annotation { + return predicate.Annotation(sql.FieldIsNull(FieldValueKey)) +} + +// ValueKeyNotNil applies the NotNil predicate on the "value_key" field. +func ValueKeyNotNil() predicate.Annotation { + return predicate.Annotation(sql.FieldNotNull(FieldValueKey)) +} + +// ValueKeyEqualFold applies the EqualFold predicate on the "value_key" field. +func ValueKeyEqualFold(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldEqualFold(FieldValueKey, v)) +} + +// ValueKeyContainsFold applies the ContainsFold predicate on the "value_key" field. +func ValueKeyContainsFold(v string) predicate.Annotation { + return predicate.Annotation(sql.FieldContainsFold(FieldValueKey, v)) +} + // HasDocument applies the HasEdge predicate on the "document" edge. func HasDocument() predicate.Annotation { return predicate.Annotation(func(s *sql.Selector) { diff --git a/internal/backends/ent/annotation_create.go b/internal/backends/ent/annotation_create.go index 6520c22..770b805 100644 --- a/internal/backends/ent/annotation_create.go +++ b/internal/backends/ent/annotation_create.go @@ -83,6 +83,20 @@ func (ac *AnnotationCreate) SetNillableIsUnique(b *bool) *AnnotationCreate { return ac } +// SetValueKey sets the "value_key" field. +func (ac *AnnotationCreate) SetValueKey(s string) *AnnotationCreate { + ac.mutation.SetValueKey(s) + return ac +} + +// SetNillableValueKey sets the "value_key" field if the given value is not nil. +func (ac *AnnotationCreate) SetNillableValueKey(s *string) *AnnotationCreate { + if s != nil { + ac.SetValueKey(*s) + } + return ac +} + // SetDocument sets the "document" edge to the Document entity. func (ac *AnnotationCreate) SetDocument(d *Document) *AnnotationCreate { return ac.SetDocumentID(d.ID) @@ -187,6 +201,10 @@ func (ac *AnnotationCreate) createSpec() (*Annotation, *sqlgraph.CreateSpec) { _spec.SetField(annotation.FieldIsUnique, field.TypeBool, value) _node.IsUnique = value } + if value, ok := ac.mutation.ValueKey(); ok { + _spec.SetField(annotation.FieldValueKey, field.TypeString, value) + _node.ValueKey = value + } if nodes := ac.mutation.DocumentIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -355,6 +373,11 @@ func (u *AnnotationUpsert) UpdateIsUnique() *AnnotationUpsert { // Exec(ctx) func (u *AnnotationUpsertOne) UpdateNewValues() *AnnotationUpsertOne { u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + if _, exists := u.create.mutation.ValueKey(); exists { + s.SetIgnore(annotation.FieldValueKey) + } + })) return u } @@ -643,6 +666,13 @@ type AnnotationUpsertBulk struct { // Exec(ctx) func (u *AnnotationUpsertBulk) UpdateNewValues() *AnnotationUpsertBulk { u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues()) + u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) { + for _, b := range u.create.builders { + if _, exists := b.mutation.ValueKey(); exists { + s.SetIgnore(annotation.FieldValueKey) + } + } + })) return u } diff --git a/internal/backends/ent/annotation_update.go b/internal/backends/ent/annotation_update.go index fd8ac94..7811a55 100644 --- a/internal/backends/ent/annotation_update.go +++ b/internal/backends/ent/annotation_update.go @@ -189,6 +189,9 @@ func (au *AnnotationUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := au.mutation.IsUnique(); ok { _spec.SetField(annotation.FieldIsUnique, field.TypeBool, value) } + if au.mutation.ValueKeyCleared() { + _spec.ClearField(annotation.FieldValueKey, field.TypeString) + } if au.mutation.DocumentCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -451,6 +454,9 @@ func (auo *AnnotationUpdateOne) sqlSave(ctx context.Context) (_node *Annotation, if value, ok := auo.mutation.IsUnique(); ok { _spec.SetField(annotation.FieldIsUnique, field.TypeBool, value) } + if auo.mutation.ValueKeyCleared() { + _spec.ClearField(annotation.FieldValueKey, field.TypeString) + } if auo.mutation.DocumentCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/internal/backends/ent/migrate/schema.go b/internal/backends/ent/migrate/schema.go index 6420f51..b46ad40 100644 --- a/internal/backends/ent/migrate/schema.go +++ b/internal/backends/ent/migrate/schema.go @@ -20,6 +20,7 @@ var ( {Name: "name", Type: field.TypeString}, {Name: "value", Type: field.TypeString}, {Name: "is_unique", Type: field.TypeBool, Default: false}, + {Name: "value_key", Type: field.TypeString, Nullable: true}, {Name: "document_id", Type: field.TypeUUID, Nullable: true}, {Name: "node_id", Type: field.TypeUUID, Nullable: true}, } @@ -31,13 +32,13 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "annotations_documents_annotations", - Columns: []*schema.Column{AnnotationsColumns[4]}, + Columns: []*schema.Column{AnnotationsColumns[5]}, RefColumns: []*schema.Column{DocumentsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "annotations_nodes_annotations", - Columns: []*schema.Column{AnnotationsColumns[5]}, + Columns: []*schema.Column{AnnotationsColumns[6]}, RefColumns: []*schema.Column{NodesColumns[0]}, OnDelete: schema.Cascade, }, @@ -46,44 +47,22 @@ var ( { Name: "idx_annotations_node_id", Unique: false, - Columns: []*schema.Column{AnnotationsColumns[5]}, + Columns: []*schema.Column{AnnotationsColumns[6]}, }, { Name: "idx_annotations_document_id", Unique: false, - Columns: []*schema.Column{AnnotationsColumns[4]}, + Columns: []*schema.Column{AnnotationsColumns[5]}, }, { Name: "idx_node_annotations", Unique: true, - Columns: []*schema.Column{AnnotationsColumns[5], AnnotationsColumns[1], AnnotationsColumns[2]}, - Annotation: &entsql.IndexAnnotation{ - Where: "node_id IS NOT NULL AND TRIM(node_id) != ''", - }, - }, - { - Name: "idx_node_unique_annotations", - Unique: true, - Columns: []*schema.Column{AnnotationsColumns[5], AnnotationsColumns[1]}, - Annotation: &entsql.IndexAnnotation{ - Where: "node_id IS NOT NULL AND TRIM(node_id) != '' AND is_unique", - }, + Columns: []*schema.Column{AnnotationsColumns[6], AnnotationsColumns[1], AnnotationsColumns[4]}, }, { Name: "idx_document_annotations", Unique: true, - Columns: []*schema.Column{AnnotationsColumns[4], AnnotationsColumns[1], AnnotationsColumns[2]}, - Annotation: &entsql.IndexAnnotation{ - Where: "document_id IS NOT NULL AND TRIM(document_id) != ''", - }, - }, - { - Name: "idx_document_unique_annotations", - Unique: true, - Columns: []*schema.Column{AnnotationsColumns[4], AnnotationsColumns[1]}, - Annotation: &entsql.IndexAnnotation{ - Where: "document_id IS NOT NULL AND TRIM(document_id) != '' AND is_unique", - }, + Columns: []*schema.Column{AnnotationsColumns[5], AnnotationsColumns[1], AnnotationsColumns[4]}, }, }, } @@ -128,7 +107,7 @@ var ( // DocumentTypesColumns holds the columns for the "document_types" table. DocumentTypesColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "type", Type: field.TypeEnum, Nullable: true, Enums: []string{"OTHER", "DESIGN", "SOURCE", "BUILD", "ANALYZED", "DEPLOYED", "RUNTIME", "DISCOVERY", "DECOMISSION"}}, {Name: "name", Type: field.TypeString, Nullable: true}, {Name: "description", Type: field.TypeString, Nullable: true}, @@ -149,7 +128,7 @@ var ( // EdgeTypesColumns holds the columns for the "edge_types" table. EdgeTypesColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "type", Type: field.TypeEnum, Enums: []string{"UNKNOWN", "amends", "ancestor", "buildDependency", "buildTool", "contains", "contained_by", "copy", "dataFile", "dependencyManifest", "dependsOn", "dependencyOf", "descendant", "describes", "describedBy", "devDependency", "devTool", "distributionArtifact", "documentation", "dynamicLink", "example", "expandedFromArchive", "fileAdded", "fileDeleted", "fileModified", "generates", "generatedFrom", "metafile", "optionalComponent", "optionalDependency", "other", "packages", "patch", "prerequisite", "prerequisiteFor", "providedDependency", "requirementFor", "runtimeDependency", "specificationFor", "staticLink", "test", "testCase", "testDependency", "testTool", "variant"}}, {Name: "node_id", Type: field.TypeUUID}, {Name: "to_node_id", Type: field.TypeUUID}, @@ -189,7 +168,7 @@ var ( // ExternalReferencesColumns holds the columns for the "external_references" table. ExternalReferencesColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "url", Type: field.TypeString}, {Name: "comment", Type: field.TypeString}, {Name: "authority", Type: field.TypeString, Nullable: true}, @@ -242,7 +221,7 @@ var ( // MetadataColumns holds the columns for the "metadata" table. MetadataColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "native_id", Type: field.TypeString}, {Name: "version", Type: field.TypeString}, {Name: "name", Type: field.TypeString}, @@ -267,7 +246,7 @@ var ( // NodesColumns holds the columns for the "nodes" table. NodesColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "native_id", Type: field.TypeString}, {Name: "type", Type: field.TypeEnum, Enums: []string{"PACKAGE", "FILE"}}, {Name: "name", Type: field.TypeString}, @@ -298,7 +277,7 @@ var ( // NodeListsColumns holds the columns for the "node_lists" table. NodeListsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "root_elements", Type: field.TypeJSON}, } // NodeListsTable holds the schema information for the "node_lists" table. @@ -310,7 +289,7 @@ var ( // PersonsColumns holds the columns for the "persons" table. PersonsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "name", Type: field.TypeString}, {Name: "is_org", Type: field.TypeBool}, {Name: "email", Type: field.TypeString}, @@ -333,7 +312,7 @@ var ( // PropertiesColumns holds the columns for the "properties" table. PropertiesColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "name", Type: field.TypeString}, {Name: "data", Type: field.TypeString}, } @@ -364,7 +343,7 @@ var ( // SourceDataColumns holds the columns for the "source_data" table. SourceDataColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "format", Type: field.TypeString}, {Name: "size", Type: field.TypeInt64}, {Name: "uri", Type: field.TypeString, Nullable: true}, @@ -385,7 +364,7 @@ var ( // ToolsColumns holds the columns for the "tools" table. ToolsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID, Unique: true}, - {Name: "proto_message", Type: field.TypeBytes, Unique: true}, + {Name: "proto_message", Type: field.TypeBytes}, {Name: "name", Type: field.TypeString}, {Name: "version", Type: field.TypeString}, {Name: "vendor", Type: field.TypeString}, diff --git a/internal/backends/ent/mutation.go b/internal/backends/ent/mutation.go index 64e15a2..c92cefc 100644 --- a/internal/backends/ent/mutation.go +++ b/internal/backends/ent/mutation.go @@ -71,6 +71,7 @@ type AnnotationMutation struct { name *string value *string is_unique *bool + value_key *string clearedFields map[string]struct{} document *uuid.UUID cleareddocument bool @@ -385,6 +386,55 @@ func (m *AnnotationMutation) ResetIsUnique() { m.is_unique = nil } +// SetValueKey sets the "value_key" field. +func (m *AnnotationMutation) SetValueKey(s string) { + m.value_key = &s +} + +// ValueKey returns the value of the "value_key" field in the mutation. +func (m *AnnotationMutation) ValueKey() (r string, exists bool) { + v := m.value_key + if v == nil { + return + } + return *v, true +} + +// OldValueKey returns the old "value_key" field's value of the Annotation entity. +// If the Annotation object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AnnotationMutation) OldValueKey(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldValueKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldValueKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldValueKey: %w", err) + } + return oldValue.ValueKey, nil +} + +// ClearValueKey clears the value of the "value_key" field. +func (m *AnnotationMutation) ClearValueKey() { + m.value_key = nil + m.clearedFields[annotation.FieldValueKey] = struct{}{} +} + +// ValueKeyCleared returns if the "value_key" field was cleared in this mutation. +func (m *AnnotationMutation) ValueKeyCleared() bool { + _, ok := m.clearedFields[annotation.FieldValueKey] + return ok +} + +// ResetValueKey resets all changes to the "value_key" field. +func (m *AnnotationMutation) ResetValueKey() { + m.value_key = nil + delete(m.clearedFields, annotation.FieldValueKey) +} + // ClearDocument clears the "document" edge to the Document entity. func (m *AnnotationMutation) ClearDocument() { m.cleareddocument = true @@ -473,7 +523,7 @@ func (m *AnnotationMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *AnnotationMutation) Fields() []string { - fields := make([]string, 0, 5) + fields := make([]string, 0, 6) if m.document != nil { fields = append(fields, annotation.FieldDocumentID) } @@ -489,6 +539,9 @@ func (m *AnnotationMutation) Fields() []string { if m.is_unique != nil { fields = append(fields, annotation.FieldIsUnique) } + if m.value_key != nil { + fields = append(fields, annotation.FieldValueKey) + } return fields } @@ -507,6 +560,8 @@ func (m *AnnotationMutation) Field(name string) (ent.Value, bool) { return m.Value() case annotation.FieldIsUnique: return m.IsUnique() + case annotation.FieldValueKey: + return m.ValueKey() } return nil, false } @@ -526,6 +581,8 @@ func (m *AnnotationMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldValue(ctx) case annotation.FieldIsUnique: return m.OldIsUnique(ctx) + case annotation.FieldValueKey: + return m.OldValueKey(ctx) } return nil, fmt.Errorf("unknown Annotation field %s", name) } @@ -570,6 +627,13 @@ func (m *AnnotationMutation) SetField(name string, value ent.Value) error { } m.SetIsUnique(v) return nil + case annotation.FieldValueKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetValueKey(v) + return nil } return fmt.Errorf("unknown Annotation field %s", name) } @@ -606,6 +670,9 @@ func (m *AnnotationMutation) ClearedFields() []string { if m.FieldCleared(annotation.FieldNodeID) { fields = append(fields, annotation.FieldNodeID) } + if m.FieldCleared(annotation.FieldValueKey) { + fields = append(fields, annotation.FieldValueKey) + } return fields } @@ -626,6 +693,9 @@ func (m *AnnotationMutation) ClearField(name string) error { case annotation.FieldNodeID: m.ClearNodeID() return nil + case annotation.FieldValueKey: + m.ClearValueKey() + return nil } return fmt.Errorf("unknown Annotation nullable field %s", name) } @@ -649,6 +719,9 @@ func (m *AnnotationMutation) ResetField(name string) error { case annotation.FieldIsUnique: m.ResetIsUnique() return nil + case annotation.FieldValueKey: + m.ResetValueKey() + return nil } return fmt.Errorf("unknown Annotation field %s", name) } diff --git a/internal/backends/ent/runtime/runtime.go b/internal/backends/ent/runtime/runtime.go index 6344eab..18120a3 100644 --- a/internal/backends/ent/runtime/runtime.go +++ b/internal/backends/ent/runtime/runtime.go @@ -196,6 +196,6 @@ func init() { } const ( - Version = "v0.14.1" // Version of ent codegen. - Sum = "h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s=" // Sum of ent codegen. + Version = "v0.14.2" // Version of ent codegen. + Sum = "h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0=" // Sum of ent codegen. ) diff --git a/internal/backends/ent/schema/annotation.go b/internal/backends/ent/schema/annotation.go index 26b0693..23724f3 100644 --- a/internal/backends/ent/schema/annotation.go +++ b/internal/backends/ent/schema/annotation.go @@ -11,7 +11,6 @@ import ( "errors" "entgo.io/ent" - "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" @@ -38,6 +37,10 @@ func (Annotation) Fields() []ent.Field { field.String("name"), field.String("value"), field.Bool("is_unique").Default(false), + field.String("value_key"). + Optional(). + Immutable(). + StorageKey("value_key"), } } @@ -66,22 +69,12 @@ func (Annotation) Indexes() []ent.Index { StorageKey("idx_annotations_node_id"), index.Fields("document_id"). StorageKey("idx_annotations_document_id"), - index.Fields("node_id", "name", "value"). + index.Fields("node_id", "name", "value_key"). Unique(). - Annotations(entsql.IndexWhere("node_id IS NOT NULL AND TRIM(node_id) != ''")). StorageKey("idx_node_annotations"), - index.Fields("node_id", "name"). - Unique(). - Annotations(entsql.IndexWhere("node_id IS NOT NULL AND TRIM(node_id) != '' AND is_unique")). - StorageKey("idx_node_unique_annotations"), - index.Fields("document_id", "name", "value"). + index.Fields("document_id", "name", "value_key"). Unique(). - Annotations(entsql.IndexWhere("document_id IS NOT NULL AND TRIM(document_id) != ''")). StorageKey("idx_document_annotations"), - index.Fields("document_id", "name"). - Unique(). - Annotations(entsql.IndexWhere("document_id IS NOT NULL AND TRIM(document_id) != '' AND is_unique")). - StorageKey("idx_document_unique_annotations"), } } @@ -96,6 +89,12 @@ func annotationHook(next ent.Mutator) ent.Mutator { return nil, errInvalidAnnotation } + if u, ok := mutation.IsUnique(); ok && u { + mutation.SetValueKey("") + } else if v, ok := mutation.Value(); ok { + mutation.SetValueKey(v) + } + return next.Mutate(ctx, mutation) }, ) diff --git a/internal/backends/ent/schema/mixin/proto_message.go b/internal/backends/ent/schema/mixin/proto_message.go index 6a97d10..b00779b 100644 --- a/internal/backends/ent/schema/mixin/proto_message.go +++ b/internal/backends/ent/schema/mixin/proto_message.go @@ -29,7 +29,6 @@ func (pm ProtoMessage[T]) Fields() []ent.Field { field.Bytes(protoMessageField). GoType(goType). Nillable(). - Unique(). Immutable(). StructTag(`json:"-"`), )