mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-23 12:16:42 +00:00
This pull request introduces a significant refactoring of the database interaction layer, focusing on improving explicitness, transactional control, and error handling. The core change is the removal of the stateful `QueryExecutor` from repository instances. Instead, it is now passed as an argument to each method that interacts with the database. This change makes transaction management more explicit and flexible, as the same repository instance can be used with a database pool or a specific transaction without needing to be re-instantiated. ### Key Changes - **Explicit `QueryExecutor` Passing:** - All repository methods (`Get`, `List`, `Create`, `Update`, `Delete`, etc.) in `InstanceRepository`, `OrganizationRepository`, `UserRepository`, and their sub-repositories now require a `database.QueryExecutor` (e.g., a `*pgxpool.Pool` or `pgx.Tx`) as the first argument. - Repository constructors no longer accept a `QueryExecutor`. For example, `repository.InstanceRepository(pool)` is now `repository.InstanceRepository()`. - **Enhanced Error Handling:** - A new `database.MissingConditionError` is introduced to enforce required query conditions, such as ensuring an `instance_id` is always present in `UPDATE` and `DELETE` operations. - The database error wrapper in the `postgres` package now correctly identifies and wraps `pgx.ErrTooManyRows` and similar errors from the `scany` library into a `database.MultipleRowsFoundError`. - **Improved Database Conditions:** - The `database.Condition` interface now includes a `ContainsColumn(Column) bool` method. This allows for runtime checks to ensure that critical filters (like `instance_id`) are included in a query, preventing accidental cross-tenant data modification. - A new `database.Exists()` condition has been added to support `EXISTS` subqueries, enabling more complex filtering logic, such as finding an organization that has a specific domain. - **Repository and Interface Refactoring:** - The method for loading related entities (e.g., domains for an organization) has been changed from a boolean flag (`Domains(true)`) to a more explicit, chainable method (`LoadDomains()`). This returns a new repository instance configured to load the sub-resource, promoting immutability. - The custom `OrgIdentifierCondition` has been removed in favor of using the standard `database.Condition` interface, simplifying the API. - **Code Cleanup and Test Updates:** - Unnecessary struct embeddings and metadata have been removed. - All integration and repository tests have been updated to reflect the new method signatures, passing the database pool or transaction object explicitly. - New tests have been added to cover the new `ExistsDomain` functionality and other enhancements. These changes make the data access layer more robust, predictable, and easier to work with, especially in the context of database transactions.
240 lines
6.3 KiB
Go
240 lines
6.3 KiB
Go
package database
|
|
|
|
// Condition represents a SQL condition.
|
|
// Its written after the WHERE keyword in a SQL statement.
|
|
type Condition interface {
|
|
Write(builder *StatementBuilder)
|
|
// IsRestrictingColumn is used to check if the condition filters for a specific column.
|
|
// It acts as a save guard database operations that should be specific on the given column.
|
|
IsRestrictingColumn(col Column) bool
|
|
}
|
|
|
|
type and struct {
|
|
conditions []Condition
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (a and) Write(builder *StatementBuilder) {
|
|
if len(a.conditions) > 1 {
|
|
builder.WriteString("(")
|
|
defer builder.WriteString(")")
|
|
}
|
|
for i, condition := range a.conditions {
|
|
if i > 0 {
|
|
builder.WriteString(" AND ")
|
|
}
|
|
condition.Write(builder)
|
|
}
|
|
}
|
|
|
|
// And combines multiple conditions with AND.
|
|
func And(conditions ...Condition) *and {
|
|
return &and{conditions: conditions}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (a and) IsRestrictingColumn(col Column) bool {
|
|
for _, condition := range a.conditions {
|
|
if condition.IsRestrictingColumn(col) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*and)(nil)
|
|
|
|
type or struct {
|
|
conditions []Condition
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (o or) Write(builder *StatementBuilder) {
|
|
if len(o.conditions) > 1 {
|
|
builder.WriteString("(")
|
|
defer builder.WriteString(")")
|
|
}
|
|
for i, condition := range o.conditions {
|
|
if i > 0 {
|
|
builder.WriteString(" OR ")
|
|
}
|
|
condition.Write(builder)
|
|
}
|
|
}
|
|
|
|
// Or combines multiple conditions with OR.
|
|
func Or(conditions ...Condition) *or {
|
|
return &or{conditions: conditions}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns true only if all conditions are restricting the given column.
|
|
func (o or) IsRestrictingColumn(col Column) bool {
|
|
for _, condition := range o.conditions {
|
|
if !condition.IsRestrictingColumn(col) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var _ Condition = (*or)(nil)
|
|
|
|
type isNull struct {
|
|
column Column
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (i isNull) Write(builder *StatementBuilder) {
|
|
i.column.WriteQualified(builder)
|
|
builder.WriteString(" IS NULL")
|
|
}
|
|
|
|
// IsNull creates a condition that checks if a column is NULL.
|
|
func IsNull(column Column) *isNull {
|
|
return &isNull{column: column}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns false because it cannot be used for restricting a column.
|
|
func (i isNull) IsRestrictingColumn(col Column) bool {
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*isNull)(nil)
|
|
|
|
type isNotNull struct {
|
|
column Column
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (i isNotNull) Write(builder *StatementBuilder) {
|
|
i.column.WriteQualified(builder)
|
|
builder.WriteString(" IS NOT NULL")
|
|
}
|
|
|
|
// IsNotNull creates a condition that checks if a column is NOT NULL.
|
|
func IsNotNull(column Column) *isNotNull {
|
|
return &isNotNull{column: column}
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
// It returns false because it cannot be used for restricting a column.
|
|
func (i isNotNull) IsRestrictingColumn(col Column) bool {
|
|
return false
|
|
}
|
|
|
|
var _ Condition = (*isNotNull)(nil)
|
|
|
|
type valueCondition struct {
|
|
write func(builder *StatementBuilder)
|
|
col Column
|
|
}
|
|
|
|
// NewTextCondition creates a condition that compares a text column with a value.
|
|
// If you want to use ignore case operations, consider using [NewTextIgnoreCaseCondition].
|
|
func NewTextCondition[T Text](col Column, op TextOperation, value T) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeTextOperation[T](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewTextIgnoreCaseCondition creates a condition that compares a text column with a value, ignoring case by lowercasing both.
|
|
func NewTextIgnoreCaseCondition[T Text](col Column, op TextOperation, value T) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeTextOperation[T](builder, LowerColumn(col), op, LowerValue(value))
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewDateCondition creates a condition that compares a numeric column with a value.
|
|
func NewNumberCondition[V Number](col Column, op NumberOperation, value V) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeNumberOperation[V](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewDateCondition creates a condition that compares a boolean column with a value.
|
|
func NewBooleanCondition[V Boolean](col Column, value V) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeBooleanOperation[V](builder, col, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewBytesCondition creates a condition that compares a BYTEA column with a value.
|
|
func NewBytesCondition[V Bytes](col Column, op BytesOperation, value any) Condition {
|
|
return valueCondition{
|
|
col: col,
|
|
write: func(builder *StatementBuilder) {
|
|
writeBytesOperation[V](builder, col, op, value)
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewColumnCondition creates a condition that compares two columns on equality.
|
|
func NewColumnCondition(col1, col2 Column) Condition {
|
|
return valueCondition{
|
|
col: col1,
|
|
write: func(builder *StatementBuilder) {
|
|
col1.WriteQualified(builder)
|
|
builder.WriteString(" = ")
|
|
col2.WriteQualified(builder)
|
|
},
|
|
}
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (c valueCondition) Write(builder *StatementBuilder) {
|
|
c.write(builder)
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (i valueCondition) IsRestrictingColumn(col Column) bool {
|
|
return i.col.Equals(col)
|
|
}
|
|
|
|
var _ Condition = (*valueCondition)(nil)
|
|
|
|
// existsCondition is a helper to write an EXISTS (SELECT 1 FROM <table> WHERE <condition>) clause.
|
|
// It implements Condition so it can be composed with other conditions using And/Or.
|
|
type existsCondition struct {
|
|
table string
|
|
condition Condition
|
|
}
|
|
|
|
// Exists creates a condition that checks for the existence of rows in a subquery.
|
|
func Exists(table string, condition Condition) Condition {
|
|
return &existsCondition{
|
|
table: table,
|
|
condition: condition,
|
|
}
|
|
}
|
|
|
|
// Write implements [Condition].
|
|
func (e existsCondition) Write(builder *StatementBuilder) {
|
|
builder.WriteString(" EXISTS (SELECT 1 FROM ")
|
|
builder.WriteString(e.table)
|
|
builder.WriteString(" WHERE ")
|
|
e.condition.Write(builder)
|
|
builder.WriteString(")")
|
|
}
|
|
|
|
// IsRestrictingColumn implements [Condition].
|
|
func (e existsCondition) IsRestrictingColumn(col Column) bool {
|
|
// Forward to the inner condition so safety checks (like instance_id presence) can still work.
|
|
return e.condition.IsRestrictingColumn(col)
|
|
}
|
|
|
|
var _ Condition = (*existsCondition)(nil)
|