snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
76
persistence/sql/update/update.go
Normal file
76
persistence/sql/update/update.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/ory/pop/v6"
|
||||
"github.com/ory/pop/v6/columns"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
func Generic(ctx context.Context, c *pop.Connection, tracer trace.Tracer, v any, columnNames ...string) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "persistence.sql.update")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
quoter, ok := c.Dialect.(interface{ Quote(key string) string })
|
||||
if !ok {
|
||||
return errors.Errorf("store is not a quoter: %T", c.Store)
|
||||
}
|
||||
|
||||
model := pop.NewModel(v, ctx)
|
||||
tn := model.TableName()
|
||||
|
||||
cols := columns.Columns{}
|
||||
if len(columnNames) > 0 && tn == model.TableName() {
|
||||
cols = columns.NewColumnsWithAlias(tn, model.As, model.IDField())
|
||||
cols.Add(columnNames...)
|
||||
} else {
|
||||
cols = columns.ForStructWithAlias(v, tn, model.As, model.IDField())
|
||||
}
|
||||
|
||||
//#nosec G201 -- TableName is static
|
||||
stmt := fmt.Sprintf("UPDATE %s AS %s SET %s WHERE (%s) AND %s.nid = :nid",
|
||||
quoter.Quote(model.TableName()),
|
||||
model.Alias(),
|
||||
cols.Writeable().QuotedUpdateString(quoter),
|
||||
model.WhereNamedID(),
|
||||
model.Alias(),
|
||||
)
|
||||
|
||||
if result, err := c.Store.NamedExecContext(ctx, stmt, v); err != nil {
|
||||
return sqlcon.HandleError(err)
|
||||
} else if affected, err := result.RowsAffected(); err != nil {
|
||||
return sqlcon.HandleError(err)
|
||||
} else if affected == 0 {
|
||||
return errors.WithStack(sqlcon.ErrNoRows())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenericExcept updates v like Generic, but omits excludeColumns from the
|
||||
// UPDATE ... SET list (every other writeable column is still written).
|
||||
//
|
||||
// Leaving a column that backs a unique index out of the SET list — when its
|
||||
// value has not changed — lets the database skip the uniqueness check it would
|
||||
// otherwise plan for that index, which can be expensive.
|
||||
func GenericExcept(ctx context.Context, c *pop.Connection, tracer trace.Tracer, v any, excludeColumns ...string) error {
|
||||
model := pop.NewModel(v, ctx)
|
||||
cols := columns.ForStructWithAlias(v, model.TableName(), model.As, model.IDField())
|
||||
cols.Remove(excludeColumns...)
|
||||
|
||||
writeable := cols.Writeable().Cols
|
||||
names := make([]string, 0, len(writeable))
|
||||
for name := range writeable {
|
||||
names = append(names, name)
|
||||
}
|
||||
return Generic(ctx, c, tracer, v, names...)
|
||||
}
|
||||
117
persistence/sql/update/update_test.go
Normal file
117
persistence/sql/update/update_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package update_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
|
||||
"github.com/ory/kratos/persistence/sql/update"
|
||||
"github.com/ory/pop/v6"
|
||||
"github.com/ory/x/dbal"
|
||||
)
|
||||
|
||||
type updateTestModel struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
NID uuid.UUID `db:"nid"`
|
||||
Name string `db:"name"`
|
||||
UniqueVal string `db:"unique_val"`
|
||||
}
|
||||
|
||||
func (updateTestModel) TableName() string { return "update_test_models" }
|
||||
|
||||
func newUpdateTestConn(t *testing.T) *pop.Connection {
|
||||
t.Helper()
|
||||
|
||||
c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dbal.NewSQLiteTestDatabase(t)})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.Open())
|
||||
t.Cleanup(func() { _ = c.Close() })
|
||||
|
||||
require.NoError(t, c.RawQuery(`CREATE TABLE update_test_models (
|
||||
id TEXT PRIMARY KEY,
|
||||
nid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
unique_val TEXT NOT NULL
|
||||
)`).Exec())
|
||||
require.NoError(t, c.RawQuery(
|
||||
`CREATE UNIQUE INDEX update_test_models_nid_unique_val ON update_test_models (nid, unique_val)`).Exec())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func TestGeneric(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
tracer := noop.NewTracerProvider().Tracer("")
|
||||
|
||||
seed := func(t *testing.T, c *pop.Connection) (nid, id uuid.UUID) {
|
||||
nid = uuid.Must(uuid.NewV4())
|
||||
id = uuid.Must(uuid.NewV4())
|
||||
require.NoError(t, c.Create(&updateTestModel{
|
||||
ID: id, NID: nid, Name: "original", UniqueVal: "original-unique",
|
||||
}))
|
||||
return nid, id
|
||||
}
|
||||
|
||||
reload := func(t *testing.T, c *pop.Connection, nid, id uuid.UUID) updateTestModel {
|
||||
var m updateTestModel
|
||||
require.NoError(t, c.Where("id = ? AND nid = ?", id, nid).First(&m))
|
||||
return m
|
||||
}
|
||||
|
||||
t.Run("case=GenericExcept leaves the excluded column untouched", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newUpdateTestConn(t)
|
||||
nid, id := seed(t, c)
|
||||
|
||||
// The in-memory model carries a different unique_val, but excluding it
|
||||
// must keep the column out of the SET list so the stored value stays.
|
||||
m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "must-not-be-written"}
|
||||
require.NoError(t, update.GenericExcept(ctx, c, tracer, m, "unique_val"))
|
||||
|
||||
got := reload(t, c, nid, id)
|
||||
assert.Equal(t, "changed", got.Name, "a non-excluded column is written")
|
||||
assert.Equal(t, "original-unique", got.UniqueVal, "the excluded column is left untouched")
|
||||
})
|
||||
|
||||
t.Run("case=Generic writes every column including the unique one", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newUpdateTestConn(t)
|
||||
nid, id := seed(t, c)
|
||||
|
||||
m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "now-written"}
|
||||
require.NoError(t, update.Generic(ctx, c, tracer, m))
|
||||
|
||||
got := reload(t, c, nid, id)
|
||||
assert.Equal(t, "changed", got.Name)
|
||||
assert.Equal(t, "now-written", got.UniqueVal, "Generic writes the unique column")
|
||||
})
|
||||
|
||||
t.Run("case=Generic with an explicit column list writes only those columns", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newUpdateTestConn(t)
|
||||
nid, id := seed(t, c)
|
||||
|
||||
m := &updateTestModel{ID: id, NID: nid, Name: "changed", UniqueVal: "must-not-be-written"}
|
||||
require.NoError(t, update.Generic(ctx, c, tracer, m, "name"))
|
||||
|
||||
got := reload(t, c, nid, id)
|
||||
assert.Equal(t, "changed", got.Name)
|
||||
assert.Equal(t, "original-unique", got.UniqueVal, "a column absent from the list is left untouched")
|
||||
})
|
||||
|
||||
t.Run("case=returns ErrNoRows when the row does not exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newUpdateTestConn(t)
|
||||
|
||||
m := &updateTestModel{ID: uuid.Must(uuid.NewV4()), NID: uuid.Must(uuid.NewV4()), Name: "x", UniqueVal: "y"}
|
||||
assert.Error(t, update.GenericExcept(ctx, c, tracer, m, "unique_val"))
|
||||
assert.Error(t, update.Generic(ctx, c, tracer, m))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user