From 02f5501baee1396c46e6a30b35c8ac771224973b Mon Sep 17 00:00:00 2001 From: Alexander Trakhimenok Date: Sun, 20 Sep 2026 19:24:17 +0100 Subject: [PATCH 1/3] feat: execute wildcard exclusion projection --- end2end/go.mod | 2 +- end2end/go.sum | 2 + go.mod | 2 +- go.sum | 2 + projection.go | 95 +++++++++++++++++++ projection_test.go | 225 +++++++++++++++++++++++++++++++++++++++++++++ reader_base.go | 51 ++++++++-- reader_records.go | 31 ++++++- sqlite_emit.go | 22 ++++- 9 files changed, 414 insertions(+), 18 deletions(-) create mode 100644 projection.go create mode 100644 projection_test.go diff --git a/end2end/go.mod b/end2end/go.mod index 5554e84..e0d4067 100644 --- a/end2end/go.mod +++ b/end2end/go.mod @@ -5,7 +5,7 @@ go 1.26.0 toolchain go1.27.1 require ( - github.com/dal-go/dalgo v0.80.11 + github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a github.com/dal-go/dalgo2sql v0.9.6 // No version as we alway replace it with local version github.com/mattn/go-sqlite3 v1.14.52 ) diff --git a/end2end/go.sum b/end2end/go.sum index 9dfba2a..87fced7 100644 --- a/end2end/go.sum +++ b/end2end/go.sum @@ -14,6 +14,8 @@ github.com/dal-go/dalgo v0.80.4 h1:qlbxd+k0vH1LlcKWIddkRVDXo2OwnBRiowUBytYw64c= github.com/dal-go/dalgo v0.80.4/go.mod h1:u7sD8jlXshi2qHrlPUm9GzR2uyPRaBI4Ro9ef5+WFzY= github.com/dal-go/dalgo v0.80.11 h1:jvbqrsnzOcYAdmgIW/bSogTg4/ERtQTk6eFiotPg1+w= github.com/dal-go/dalgo v0.80.11/go.mod h1:+6651I975Yh2hoh+eyiqrI19VHC2lWFQNhN0iQmjZQ0= +github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a h1:t6huw80nzExaDGr4mBowB0Km6jVDD0te31fqHSgCTpE= +github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a/go.mod h1:+6651I975Yh2hoh+eyiqrI19VHC2lWFQNhN0iQmjZQ0= github.com/dal-go/record v0.1.3 h1:K85k/kSX08lTefMiMmPpC12nmCY2TVJk7+ZyC5PD9XU= github.com/dal-go/record v0.1.3/go.mod h1:quwsVJTT0f6y3Mhx+yHpTobY7luX1M6kyO6fdJ/AFYE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= diff --git a/go.mod b/go.mod index 9fba08c..c53f8b5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.27.1 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/dal-go/dalgo v0.80.11 + github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a github.com/dal-go/record v0.1.3 github.com/georgysavva/scany/v2 v2.1.4 modernc.org/sqlite v1.59.0 diff --git a/go.sum b/go.sum index fdb34b4..6bb584d 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/dal-go/dalgo v0.80.4 h1:qlbxd+k0vH1LlcKWIddkRVDXo2OwnBRiowUBytYw64c= github.com/dal-go/dalgo v0.80.4/go.mod h1:u7sD8jlXshi2qHrlPUm9GzR2uyPRaBI4Ro9ef5+WFzY= github.com/dal-go/dalgo v0.80.11 h1:jvbqrsnzOcYAdmgIW/bSogTg4/ERtQTk6eFiotPg1+w= github.com/dal-go/dalgo v0.80.11/go.mod h1:+6651I975Yh2hoh+eyiqrI19VHC2lWFQNhN0iQmjZQ0= +github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a h1:t6huw80nzExaDGr4mBowB0Km6jVDD0te31fqHSgCTpE= +github.com/dal-go/dalgo v0.81.1-0.20260920182151-52a700c3b99a/go.mod h1:+6651I975Yh2hoh+eyiqrI19VHC2lWFQNhN0iQmjZQ0= github.com/dal-go/record v0.1.3 h1:K85k/kSX08lTefMiMmPpC12nmCY2TVJk7+ZyC5PD9XU= github.com/dal-go/record v0.1.3/go.mod h1:quwsVJTT0f6y3Mhx+yHpTobY7luX1M6kyO6fdJ/AFYE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= diff --git a/projection.go b/projection.go new file mode 100644 index 0000000..3549f7e --- /dev/null +++ b/projection.go @@ -0,0 +1,95 @@ +package dalgo2sql + +import ( + "fmt" + + "github.com/dal-go/dalgo/dal" +) + +type wildcardProjectionPlan struct { + qualifier string + excluded map[string]struct{} + explicitCount int +} + +func planWildcardProjection(q dal.StructuredQuery) (*wildcardProjectionPlan, error) { + columns := q.Columns() + var projection *dal.WildcardProjection + for i, column := range columns { + if column.Wildcard == nil { + continue + } + if projection != nil { + return nil, fmt.Errorf("multiple wildcard projections require join support") + } + if i != 0 { + return nil, fmt.Errorf("wildcard projection must precede explicit columns") + } + if column.Expression != nil { + return nil, fmt.Errorf("wildcard projection cannot also contain an expression") + } + if column.Alias != "" { + return nil, fmt.Errorf("wildcard projection cannot have an alias") + } + projection = column.Wildcard + } + if projection == nil { + return nil, nil + } + if len(projection.Exclude) == 0 { + return nil, fmt.Errorf("wildcard projection requires at least one exclusion") + } + if q.From() == nil || q.From().Base() == nil { + return nil, fmt.Errorf("wildcard projection requires a source") + } + if len(q.From().Joins()) != 0 { + return nil, fmt.Errorf("wildcard projection with joins is not supported") + } + base := q.From().Base() + qualifier := "" + if projection.Source != "" { + if projection.Source != base.Alias() && projection.Source != base.Name() { + return nil, fmt.Errorf("wildcard projection source %q does not match source name or alias", projection.Source) + } + qualifier = base.Alias() + if qualifier == "" { + qualifier = base.Name() + } + } + excluded := make(map[string]struct{}, len(projection.Exclude)) + for i, name := range projection.Exclude { + if name == "" { + return nil, fmt.Errorf("wildcard projection exclusion %d is empty", i) + } + excluded[name] = struct{}{} + } + return &wildcardProjectionPlan{ + qualifier: qualifier, + excluded: excluded, + explicitCount: len(columns) - 1, + }, nil +} + +func (p wildcardProjectionPlan) sqlExpression(quote func(string) string) string { + if p.qualifier == "" { + return "*" + } + return quote(p.qualifier) + ".*" +} + +func (p wildcardProjectionPlan) visibleIndexes(columnNames []string) ([]int, error) { + wildcardEnd := len(columnNames) - p.explicitCount + if wildcardEnd < 0 { + return nil, fmt.Errorf("SQL result has %d columns, fewer than %d explicit projections", len(columnNames), p.explicitCount) + } + indexes := make([]int, 0, len(columnNames)) + for i, name := range columnNames { + if i < wildcardEnd { + if _, excluded := p.excluded[name]; excluded { + continue + } + } + indexes = append(indexes, i) + } + return indexes, nil +} diff --git a/projection_test.go b/projection_test.go new file mode 100644 index 0000000..4471ca0 --- /dev/null +++ b/projection_test.go @@ -0,0 +1,225 @@ +package dalgo2sql + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/dal-go/dalgo/dal" +) + +func TestCompileStructuredSQLWildcardExclusion(t *testing.T) { + tests := []struct { + name string + q dal.StructuredQuery + want string + }{ + { + name: "unqualified", + q: dal.From(dal.NewRootCollectionRef("customers", "")).NewQuery(). + SelectColumns(dal.AllColumnsExcept("email", "missing")), + want: "SELECT * FROM `customers`", + }, + { + name: "qualified", + q: dal.From(dal.NewRootCollectionRef("customers", "c")).NewQuery(). + SelectColumns(dal.AllColumnsExceptFrom("c", "email")), + want: "SELECT `c`.* FROM `customers` AS `c`", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, args, err := compileStructuredSQL(tt.q) + if err != nil { + t.Fatal(err) + } + if got != tt.want || len(args) != 0 { + t.Fatalf("compileStructuredSQL() = %q, %#v; want %q, no args", got, args, tt.want) + } + }) + } +} + +func TestEmitSQLQualifiedWildcardExclusionUsesLegacySingleSourceWildcard(t *testing.T) { + q := dal.From(dal.NewRootCollectionRef("customers", "c")).NewQuery(). + SelectColumns(dal.AllColumnsExceptFrom("c", "email")) + if got, want := emitSQL(q), "SELECT * FROM customers"; got != want { + t.Fatalf("emitSQL() = %q, want %q", got, want) + } +} + +func TestWildcardExclusionRecordsReader(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatal(err) + } + defer closeDatabase(t, db) + + q := dal.From(dal.NewRootCollectionRef("customers", "c")).NewQuery(). + SelectColumns(dal.AllColumnsExceptFrom("c", "email", "password_hash", "email")) + mock.ExpectQuery("SELECT `c`.* FROM `customers` AS `c`"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "email", "created_at"}). + AddRow("c1", "Ada", "ada@example.test", "2026-09-20")) + + r, err := getRecordsReaderWithOptions(context.Background(), q, db.QueryContext, DbOptions{ + StructuredQueryDialect: "sqlite", + Recordsets: map[string]*Recordset{ + "customers": NewRecordset("customers", Table, []dal.FieldRef{dal.Field("id")}), + }, + }) + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + record, err := r.Next() + if err != nil { + t.Fatal(err) + } + want := map[string]any{"id": "c1", "name": "Ada", "created_at": "2026-09-20"} + if got := record.Data().(map[string]any); !reflect.DeepEqual(got, want) { + t.Fatalf("record data = %#v, want %#v", got, want) + } + if record.Key().ID != "c1" { + t.Fatalf("record key ID = %#v, want c1", record.Key().ID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestWildcardExclusionRecordsetReaderPreservesOrder(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatal(err) + } + defer closeDatabase(t, db) + + q := dal.From(dal.NewRootCollectionRef("customers", "")).NewQuery(). + SelectColumns(dal.AllColumnsExcept("email", "missing")) + mock.ExpectQuery("SELECT * FROM `customers`"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "email", "created_at"}). + AddRow([]byte("c1"), []byte("Ada"), []byte("ada@example.test"), []byte("2026-09-20"))) + + r, err := getRecordsetReaderWithDialect(context.Background(), q, db.QueryContext, "sqlite") + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + if got := r.Recordset().Columns(); len(got) != 3 || got[0].Name() != "id" || got[1].Name() != "name" || got[2].Name() != "created_at" { + t.Fatalf("recordset columns = %#v", got) + } + row, rs, err := r.Next() + if err != nil { + t.Fatal(err) + } + for i, want := range []string{"c1", "Ada", "2026-09-20"} { + value, valueErr := row.GetValueByIndex(i, rs) + if valueErr != nil { + t.Fatal(valueErr) + } + if stringValue := valueString(value); stringValue != want { + t.Fatalf("value %d = %q, want %q", i, stringValue, want) + } + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func valueString(value any) string { + if bytes, ok := value.([]byte); ok { + return string(bytes) + } + return value.(string) +} + +func TestWildcardProjectionValidation(t *testing.T) { + base := dal.From(dal.NewRootCollectionRef("customers", "c")).NewQuery() + tests := []struct { + name string + columns []dal.Column + }{ + {name: "empty exclusions", columns: []dal.Column{dal.AllColumnsExcept()}}, + {name: "empty name", columns: []dal.Column{dal.AllColumnsExcept("")}}, + {name: "unknown source", columns: []dal.Column{dal.AllColumnsExceptFrom("x", "email")}}, + {name: "alias", columns: []dal.Column{{Alias: "rest", Wildcard: &dal.WildcardProjection{Exclude: []string{"email"}}}}}, + {name: "expression", columns: []dal.Column{{Expression: dal.Field("id"), Wildcard: &dal.WildcardProjection{Exclude: []string{"email"}}}}}, + {name: "multiple", columns: []dal.Column{dal.AllColumnsExcept("email"), dal.AllColumnsExcept("secret")}}, + {name: "not first", columns: []dal.Column{{Expression: dal.Field("id")}, dal.AllColumnsExcept("email")}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, _, err := compileStructuredSQL(base.SelectColumns(tt.columns...)); err == nil { + t.Fatal("expected validation error") + } + }) + } +} + +func TestWildcardProjectionRejectsJoinedSources(t *testing.T) { + from := dal.From(dal.NewRootCollectionRef("customers", "c")).Join( + dal.NewJoinedSource(dal.NewRootCollectionRef("orders", "o"), dal.JoinInner), + ) + q := from.NewQuery().SelectColumns(dal.AllColumnsExceptFrom("c", "email")) + if _, err := planWildcardProjection(q); err == nil { + t.Fatal("expected joined wildcard projection to be rejected") + } +} + +func TestWildcardProjectionKeepsExplicitTail(t *testing.T) { + q := dal.From(dal.NewRootCollectionRef("customers", "")).NewQuery().SelectColumns( + dal.AllColumnsExcept("email"), + dal.Column{Expression: dal.Field("id"), Alias: "helper"}, + ) + plan, err := planWildcardProjection(q) + if err != nil { + t.Fatal(err) + } + got, err := plan.visibleIndexes([]string{"id", "email", "helper"}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, []int{0, 2}) { + t.Fatalf("visible indexes = %#v, want [0 2]", got) + } +} + +func TestWildcardExclusionIdentityHelperDoesNotHideSameNamedSourceColumn(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Fatal(err) + } + defer closeDatabase(t, db) + + q := dal.From(dal.NewRootCollectionRef("customers", "")).NewQuery(). + SelectColumns(dal.AllColumnsExcept("id")) + mock.ExpectQuery("SELECT *, `id` AS `__dalgo_record_id` FROM `customers`"). + WillReturnRows(sqlmock.NewRows([]string{"id", "__dalgo_record_id", "__dalgo_record_id"}). + AddRow("c1", "source-value", "c1")) + + r, err := getRecordsReaderWithOptions(context.Background(), q, db.QueryContext, DbOptions{ + StructuredQueryDialect: "sqlite", + Recordsets: map[string]*Recordset{ + "customers": NewRecordset("customers", Table, []dal.FieldRef{dal.Field("id")}), + }, + }) + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + record, err := r.Next() + if err != nil { + t.Fatal(err) + } + if record.Key().ID != "c1" { + t.Fatalf("record key ID = %#v, want c1", record.Key().ID) + } + want := map[string]any{"__dalgo_record_id": "source-value"} + if got := record.Data().(map[string]any); !reflect.DeepEqual(got, want) { + t.Fatalf("record data = %#v, want %#v", got, want) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} diff --git a/reader_base.go b/reader_base.go index 0a8f7a2..e82ff3d 100644 --- a/reader_base.go +++ b/reader_base.go @@ -12,9 +12,12 @@ import ( type executeQueryFunc func(ctx context.Context, query string, args ...any) (*sql.Rows, error) type readerBase struct { - rows *sql.Rows - colNames []string - colTypes []*sql.ColumnType + rows *sql.Rows + colNames []string + colTypes []*sql.ColumnType + scanColNames []string + scanColTypes []*sql.ColumnType + visibleIndexes []int } func getReaderBase(ctx context.Context, query dal.Query, execute executeQueryFunc) (readerBase, error) { @@ -24,6 +27,7 @@ func getReaderBase(ctx context.Context, query dal.Query, execute executeQueryFun func getReaderBaseWithDialect(ctx context.Context, query dal.Query, execute executeQueryFunc, dialect string) (readerBase, error) { var a []any var text string + var projection *wildcardProjectionPlan switch q := query.(type) { case dal.TextQuery: text = q.Text() @@ -45,6 +49,10 @@ func getReaderBaseWithDialect(ctx context.Context, query dal.Query, execute exec } } case dal.StructuredQuery: + var err error + if projection, err = planWildcardProjection(q); err != nil { + return readerBase{}, err + } switch dialect { case "": text = emitSQL(q) @@ -66,26 +74,49 @@ func getReaderBaseWithDialect(ctx context.Context, query dal.Query, execute exec rb := readerBase{ rows: rows, } - if rb.colNames, err = rb.rows.Columns(); err != nil { + if rb.scanColNames, err = rb.rows.Columns(); err != nil { + _ = rb.rows.Close() return rb, fmt.Errorf("failed to read column names: %w", err) } - if rb.colTypes, err = rb.rows.ColumnTypes(); err != nil { + if rb.scanColTypes, err = rb.rows.ColumnTypes(); err != nil { + _ = rb.rows.Close() return rb, fmt.Errorf("failed to read column types: %w", err) } - if len(rb.colNames) != len(rb.colTypes) { + if len(rb.scanColNames) != len(rb.scanColTypes) { + _ = rb.rows.Close() return rb, fmt.Errorf("length if column names and column types don't match") } + rb.visibleIndexes = make([]int, len(rb.scanColNames)) + for i := range rb.visibleIndexes { + rb.visibleIndexes[i] = i + } + if projection != nil { + if rb.visibleIndexes, err = projection.visibleIndexes(rb.scanColNames); err != nil { + _ = rb.rows.Close() + return rb, err + } + } + rb.colNames = make([]string, len(rb.visibleIndexes)) + rb.colTypes = make([]*sql.ColumnType, len(rb.visibleIndexes)) + for i, sourceIndex := range rb.visibleIndexes { + rb.colNames[i] = rb.scanColNames[sourceIndex] + rb.colTypes[i] = rb.scanColTypes[sourceIndex] + } return rb, nil } func (rb readerBase) scanValues() (values []any, err error) { - values = make([]any, len(rb.colNames)) - scanArgs := make([]any, len(rb.colNames)) - for i := range values { - scanArgs[i] = &values[i] + rawValues := make([]any, len(rb.scanColNames)) + scanArgs := make([]any, len(rb.scanColNames)) + for i := range rawValues { + scanArgs[i] = &rawValues[i] } if err = rb.rows.Scan(scanArgs...); err != nil { return nil, err } + values = make([]any, len(rb.visibleIndexes)) + for i, sourceIndex := range rb.visibleIndexes { + values[i] = rawValues[sourceIndex] + } return values, nil } diff --git a/reader_records.go b/reader_records.go index 1d55fe0..70b8818 100644 --- a/reader_records.go +++ b/reader_records.go @@ -20,6 +20,7 @@ const recordIDHelperColumn = "__dalgo_record_id" func getRecordsReaderWithOptions(ctx context.Context, query dal.Query, execute executeQueryFunc, options DbOptions) (rr *recordsReader, err error) { rr = &recordsReader{ + identityColumnIndex: -1, newRecord: func() dalrecord.Record { return dalrecord.NewRecordWithData(dalrecord.NewKeyWithID("Unknown", ""), make(map[string]any)) }, @@ -50,19 +51,35 @@ func getRecordsReaderWithOptions(ctx context.Context, query dal.Query, execute e err = fmt.Errorf("failed to get SQL reader: %w", err) return } + if rr.hideIdentityColumn { + rr.identityColumnIndex = len(rr.colNames) - 1 + } return } type recordsReader struct { readerBase - newRecord func() dalrecord.Record - identityColumn string - hideIdentityColumn bool + newRecord func() dalrecord.Record + identityColumn string + identityColumnIndex int + hideIdentityColumn bool } func selectsIdentityField(columns []dal.Column, name string) bool { for _, column := range columns { + if column.Wildcard != nil { + excluded := false + for _, excludedName := range column.Wildcard.Exclude { + if excludedName == name { + excluded = true + break + } + } + if !excluded { + return true + } + } if field, ok := column.Expression.(dal.FieldRef); ok && field.Name() == name && (column.Alias == "" || column.Alias == name) { return true } @@ -124,13 +141,17 @@ func (r recordsReader) Next() (record dalrecord.Record, err error) { if b, ok := v.([]byte); ok { v = string(b) } - if n == r.identityColumn { + identityValue := n == r.identityColumn + if r.hideIdentityColumn { + identityValue = i == r.identityColumnIndex + } + if identityValue { record.Key().ID = v if v != nil { record.Key().IDKind = reflect.TypeOf(v).Kind() } } - if !r.hideIdentityColumn || n != r.identityColumn { + if !r.hideIdentityColumn || i != r.identityColumnIndex { d[n] = v } } diff --git a/sqlite_emit.go b/sqlite_emit.go index 51d11a9..ca7e785 100644 --- a/sqlite_emit.go +++ b/sqlite_emit.go @@ -42,6 +42,10 @@ func compileStructuredSQL(q dal.StructuredQuery) (string, []any, error) { var b strings.Builder b.WriteString("SELECT ") var args []any + wildcard, err := planWildcardProjection(q) + if err != nil { + return "", nil, err + } if columns := q.Columns(); len(columns) == 0 { b.WriteByte('*') } else { @@ -49,6 +53,10 @@ func compileStructuredSQL(q dal.StructuredQuery) (string, []any, error) { if i > 0 { b.WriteString(", ") } + if column.Wildcard != nil { + b.WriteString(wildcard.sqlExpression(quoteSQLIdentifier)) + continue + } expr, values, err := compileSQLExpression(column.Expression, sourceAlias) if err != nil { return "", nil, fmt.Errorf("column %d: %w", i, err) @@ -348,7 +356,19 @@ func validateSQLValue(value any) error { // emitSQL preserves the historical string-rewrite helper for compatibility. // Structured query execution uses compileStructuredSQL instead. func emitSQL(q dal.StructuredQuery) string { - text := stripBracketIdents(q.String()) + text := q.String() + if wildcard, err := planWildcardProjection(q); err == nil && wildcard != nil { + for _, column := range q.Columns() { + if column.Wildcard != nil { + // QueryString's legacy FROM rendering does not emit source aliases. + // This path supports only a single source, so an unqualified wildcard + // is equivalent and avoids producing an alias that is absent from FROM. + text = strings.Replace(text, column.String(), "*", 1) + break + } + } + } + text = stripBracketIdents(text) if limit := q.Limit(); limit > 0 { top := fmt.Sprintf("SELECT TOP %d", limit) if strings.HasPrefix(text, top) { From 15f5993c01adea31f948d5fdbcd056910548515f Mon Sep 17 00:00:00 2001 From: Alexander Trakhimenok Date: Sun, 20 Sep 2026 20:57:55 +0100 Subject: [PATCH 2/3] ci: gate Qodana with repository variable --- .github/workflows/qodana_code_quality.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index 2debdb5..3e7c2e8 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -9,7 +9,7 @@ on: jobs: qodana: - if: false + if: ${{ vars.QODANA_ENABLED == 'true' }} runs-on: ubuntu-latest permissions: contents: write @@ -26,4 +26,4 @@ jobs: pr-mode: false env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_703174159 }} - QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file + QODANA_ENDPOINT: 'https://qodana.cloud' From 12b0c9a202ac6459f6d4e8abd81af267c477a651 Mon Sep 17 00:00:00 2001 From: Alexander Trakhimenok Date: Sun, 20 Sep 2026 21:17:54 +0100 Subject: [PATCH 3/3] ci: stop disabled Qodana auto-runs --- .github/workflows/qodana_code_quality.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml index 3e7c2e8..4cbff12 100644 --- a/.github/workflows/qodana_code_quality.yml +++ b/.github/workflows/qodana_code_quality.yml @@ -1,11 +1,6 @@ name: Qodana on: workflow_dispatch: - pull_request: - push: - branches: # Specify your branches here - - main # The 'main' branch - - 'releases/*' # The release branches jobs: qodana: