mirror of
https://github.com/zitadel/zitadel.git
synced 2026-08-17 16:35:14 -05:00
feat(database): add statement and CTE changes (#11069)
Read pull request "feat(database): add statement and CTE changes (#11069)" This pull request introduces new functionality for handling database changes using statements and Common Table Expressions (CTEs). ### Summary This PR accomplishes the following: - **Statement and CTE Changes**: Adds new types and functions in change.go for handling more complex database changes. This includes: - `NewChangeToNull`: A change that sets a column to `NULL`. - `NewChangeToColumn`: A change that sets a column's value to the value of another column. - `NewIncrementColumnChange`: A change that increments a column's value by 1. - `NewChangeToStatement`: A change that sets a column's value to the result of a subquery. - `NewCTEChange`: A change that uses a Common Table Expression (CTE) to perform more complex updates. - **Testing**: Adds comprehensive tests in change_test.go to cover the new functionality and ensure the correctness of the generated SQL statements and arguments. - **Ignoring AI Files**: Updates the .gitignore to exclude files generated by AI tools. This work enhances the flexibility of the database layer, allowing for more complex and efficient database operations.
This commit is contained in:
@@ -97,3 +97,9 @@ dist
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.cursor/rules/nx-rules.mdc
|
||||
|
||||
# AI Files
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
.mcp.json
|
||||
.gemini/*
|
||||
|
||||
@@ -119,3 +119,217 @@ func (c Changes) String() string {
|
||||
}
|
||||
|
||||
var _ Change = Changes(nil)
|
||||
|
||||
func NewChangeToNull(col Column) Change {
|
||||
return NewChange(col, NullInstruction)
|
||||
}
|
||||
|
||||
func NewChangeToColumn(to, from Column) Change {
|
||||
return &changeToColumn{to: to, from: from}
|
||||
}
|
||||
|
||||
type changeToColumn struct {
|
||||
to Column
|
||||
from Column
|
||||
}
|
||||
|
||||
// IsOnColumn implements [Change].
|
||||
func (c *changeToColumn) IsOnColumn(col Column) bool {
|
||||
return c.to.Equals(col)
|
||||
}
|
||||
|
||||
// Matches implements [Change].
|
||||
func (c *changeToColumn) Matches(x any) bool {
|
||||
toMatch, ok := x.(*changeToColumn)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.to.Equals(toMatch.to) && c.from.Equals(toMatch.from)
|
||||
}
|
||||
|
||||
// String implements [Change].
|
||||
func (c *changeToColumn) String() string {
|
||||
return "database.changeToColumn"
|
||||
}
|
||||
|
||||
// Write implements [Change].
|
||||
func (c *changeToColumn) Write(builder *StatementBuilder) {
|
||||
c.to.WriteUnqualified(builder)
|
||||
builder.WriteString(" = ")
|
||||
c.from.WriteQualified(builder)
|
||||
}
|
||||
|
||||
var _ Change = (*changeToColumn)(nil)
|
||||
|
||||
type incrementColumnChange struct {
|
||||
column Column
|
||||
}
|
||||
|
||||
func NewIncrementColumnChange(col Column) Change {
|
||||
return &incrementColumnChange{
|
||||
column: col,
|
||||
}
|
||||
}
|
||||
|
||||
// IsOnColumn implements [Change].
|
||||
func (i *incrementColumnChange) IsOnColumn(col Column) bool {
|
||||
return i.column.Equals(col)
|
||||
}
|
||||
|
||||
// Matches implements [Change].
|
||||
func (i *incrementColumnChange) Matches(x any) bool {
|
||||
toMatch, ok := x.(*incrementColumnChange)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return i.column.Equals(toMatch.column)
|
||||
}
|
||||
|
||||
// String implements [Change].
|
||||
func (i *incrementColumnChange) String() string {
|
||||
return "database.incrementColumnChange"
|
||||
}
|
||||
|
||||
// Write implements [Change].
|
||||
func (i *incrementColumnChange) Write(builder *StatementBuilder) {
|
||||
i.column.WriteUnqualified(builder)
|
||||
builder.WriteString(" = ")
|
||||
i.column.WriteUnqualified(builder)
|
||||
builder.WriteString(" + 1")
|
||||
}
|
||||
|
||||
var _ Change = (*incrementColumnChange)(nil)
|
||||
|
||||
func NewChangeToStatement(col Column, stmt func(builder *StatementBuilder)) Change {
|
||||
return &changeToStatement{
|
||||
column: col,
|
||||
stmt: stmt,
|
||||
}
|
||||
}
|
||||
|
||||
type changeToStatement struct {
|
||||
column Column
|
||||
stmt func(builder *StatementBuilder)
|
||||
}
|
||||
|
||||
// IsOnColumn implements [Change].
|
||||
func (c *changeToStatement) IsOnColumn(col Column) bool {
|
||||
return c.column.Equals(col)
|
||||
}
|
||||
|
||||
// Matches implements [Change].
|
||||
func (c *changeToStatement) Matches(x any) bool {
|
||||
toMatch, ok := x.(*changeToStatement)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
var expectedBuilder, actualBuilder StatementBuilder
|
||||
c.stmt(&expectedBuilder)
|
||||
toMatch.stmt(&actualBuilder)
|
||||
|
||||
if expectedBuilder.String() != actualBuilder.String() {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(expectedBuilder.Args(), actualBuilder.Args()) {
|
||||
return false
|
||||
}
|
||||
return c.column.Equals(toMatch.column)
|
||||
}
|
||||
|
||||
// String implements [Change].
|
||||
func (c *changeToStatement) String() string {
|
||||
return "database.changeToStatement"
|
||||
}
|
||||
|
||||
// Write implements [Change].
|
||||
func (c *changeToStatement) Write(builder *StatementBuilder) {
|
||||
_, ok := c.column.(Columns)
|
||||
if ok {
|
||||
builder.WriteRune('(')
|
||||
}
|
||||
c.column.WriteUnqualified(builder)
|
||||
if ok {
|
||||
builder.WriteRune(')')
|
||||
}
|
||||
builder.WriteString(" = (")
|
||||
c.stmt(builder)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
var _ Change = (*changeToStatement)(nil)
|
||||
|
||||
// CTEChange represents a change that uses a Common Table Expression (CTE).
|
||||
// It intercepts the Write process to first write the CTE part, and then the main change part.
|
||||
type CTEChange interface {
|
||||
Change
|
||||
// WriteCTE writes the CTE part of the change to the given statement builder.
|
||||
// It writes the part inside the brackets meaning without the "WITH cte_name AS (" and the ending ")".
|
||||
WriteCTE(builder *StatementBuilder)
|
||||
// SetName sets the name of the CTE.
|
||||
// This is defined by the caller to ensure uniqueness.
|
||||
// The name is used to reference the CTE in the main change.
|
||||
SetName(name string)
|
||||
}
|
||||
|
||||
func NewCTEChange(cte func(builder *StatementBuilder), change func(name string) Change) CTEChange {
|
||||
return &cteChange{
|
||||
cte: cte,
|
||||
change: change,
|
||||
}
|
||||
}
|
||||
|
||||
type cteChange struct {
|
||||
name string
|
||||
cte func(builder *StatementBuilder)
|
||||
change func(name string) Change
|
||||
}
|
||||
|
||||
// IsOnColumn implements [CTEChange].
|
||||
func (c *cteChange) IsOnColumn(col Column) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Matches implements [CTEChange].
|
||||
func (c *cteChange) Matches(x any) bool {
|
||||
toMatch, ok := x.(*cteChange)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
var expectedCTEBuilder, actualCTEBuilder StatementBuilder
|
||||
c.cte(&expectedCTEBuilder)
|
||||
if c.change != nil {
|
||||
c.change(c.name).Write(&expectedCTEBuilder)
|
||||
}
|
||||
toMatch.cte(&actualCTEBuilder)
|
||||
if toMatch.change != nil {
|
||||
toMatch.change(toMatch.name).Write(&actualCTEBuilder)
|
||||
}
|
||||
|
||||
if expectedCTEBuilder.String() != actualCTEBuilder.String() {
|
||||
return false
|
||||
}
|
||||
return slices.Equal(expectedCTEBuilder.Args(), actualCTEBuilder.Args())
|
||||
}
|
||||
|
||||
// Name implements [CTEChange].
|
||||
func (c *cteChange) SetName(name string) {
|
||||
c.name = name
|
||||
}
|
||||
|
||||
// String implements [CTEChange].
|
||||
func (c *cteChange) String() string {
|
||||
return "database.cteChange"
|
||||
}
|
||||
|
||||
// Write implements [CTEChange].
|
||||
func (c *cteChange) Write(builder *StatementBuilder) {
|
||||
if c.change == nil {
|
||||
return
|
||||
}
|
||||
c.change(c.name).Write(builder)
|
||||
}
|
||||
|
||||
// WriteCTE implements [CTEChange].
|
||||
func (c *cteChange) WriteCTE(builder *StatementBuilder) {
|
||||
c.cte(builder)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ func TestChangeWrite(t *testing.T) {
|
||||
args: []any{"value1", 123},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "increment change",
|
||||
change: NewIncrementColumnChange(NewColumn("table", "counter")),
|
||||
want: want{
|
||||
stmt: "counter = counter + 1",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var builder StatementBuilder
|
||||
@@ -66,3 +73,186 @@ func TestChangeWrite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeToStatement(t *testing.T) {
|
||||
type want struct {
|
||||
stmt string
|
||||
args []any
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
prefillBuilder func(builder *StatementBuilder)
|
||||
change *changeToStatement
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "change to statement",
|
||||
change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT 1")
|
||||
}).(*changeToStatement),
|
||||
want: want{
|
||||
stmt: "column = (SELECT 1)",
|
||||
args: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change to statement with args",
|
||||
change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT ")
|
||||
builder.WriteArg(42)
|
||||
}).(*changeToStatement),
|
||||
want: want{
|
||||
stmt: "column = (SELECT $1)",
|
||||
args: []any{42},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change to statement with existing builder args",
|
||||
prefillBuilder: func(builder *StatementBuilder) {
|
||||
builder.WriteString("UPDATE table SET ")
|
||||
NewChanges(
|
||||
NewChange(NewColumn("table", "field1"), "asdf"),
|
||||
NewChangeToNull(NewColumn("table", "field2")),
|
||||
NewChangeToColumn(NewColumn("table", "field3"), NewColumn("table", "field4")),
|
||||
).Write(builder)
|
||||
builder.WriteString(", ")
|
||||
},
|
||||
change: NewChangeToStatement(NewColumn("table", "column"), func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT ")
|
||||
builder.WriteArg(42)
|
||||
builder.WriteString(" FROM other_table WHERE ")
|
||||
NewBooleanCondition(NewColumn("table", "id"), true).Write(builder)
|
||||
}).(*changeToStatement),
|
||||
want: want{
|
||||
stmt: "UPDATE table SET field1 = $1, field2 = NULL, field3 = table.field4, column = (SELECT $2 FROM other_table WHERE table.id = $3)",
|
||||
args: []any{"asdf", 42, true},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var builder StatementBuilder
|
||||
if test.prefillBuilder != nil {
|
||||
test.prefillBuilder(&builder)
|
||||
}
|
||||
test.change.Write(&builder)
|
||||
assert.Equal(t, test.want.stmt, builder.String())
|
||||
assert.Equal(t, builder.Args(), test.want.args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCTEChange(t *testing.T) {
|
||||
type want struct {
|
||||
stmt string
|
||||
args []any
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
prefillBuilder func(builder *StatementBuilder)
|
||||
afterCTE func(builder *StatementBuilder)
|
||||
change *cteChange
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "only CTE change",
|
||||
change: NewCTEChange(
|
||||
func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT 1 AS value")
|
||||
},
|
||||
nil,
|
||||
).(*cteChange),
|
||||
want: want{
|
||||
stmt: "WITH cte AS (SELECT 1 AS value) ",
|
||||
args: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with existing CTE",
|
||||
prefillBuilder: func(builder *StatementBuilder) {
|
||||
builder.WriteString("existing_cte AS (SELECT ")
|
||||
builder.WriteArg(42)
|
||||
builder.WriteString(" AS reason), ")
|
||||
},
|
||||
change: NewCTEChange(
|
||||
func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT ")
|
||||
Columns{
|
||||
NewColumn("table", "column1"),
|
||||
NewColumn("table", "column2"),
|
||||
}.WriteQualified(builder)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteArgs("asdf", 123, false, NullInstruction)
|
||||
},
|
||||
nil,
|
||||
).(*cteChange),
|
||||
want: want{
|
||||
stmt: "WITH existing_cte AS (SELECT $1 AS reason), cte AS (SELECT table.column1, table.column2, $2, $3, $4, NULL) ",
|
||||
args: []any{42, "asdf", 123, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CTE change with after CTE statements",
|
||||
change: NewCTEChange(
|
||||
func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT ")
|
||||
builder.WriteArg(1)
|
||||
builder.WriteString(" AS value")
|
||||
},
|
||||
nil,
|
||||
).(*cteChange),
|
||||
afterCTE: func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT * FROM cte ")
|
||||
},
|
||||
want: want{
|
||||
stmt: "WITH cte AS (SELECT $1 AS value) SELECT * FROM cte ",
|
||||
args: []any{1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CTE change with column change after CTE",
|
||||
change: NewCTEChange(
|
||||
func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT ")
|
||||
builder.WriteArg(1)
|
||||
builder.WriteString(" AS value")
|
||||
},
|
||||
func(name string) Change {
|
||||
return NewChangeToStatement(NewColumn("table", "field"), func(builder *StatementBuilder) {
|
||||
builder.WriteString("SELECT * FROM ")
|
||||
builder.WriteString(name)
|
||||
builder.WriteString(" WHERE ")
|
||||
NewColumnCondition(NewColumn(name, "value"), NewColumn("table", "value")).Write(builder)
|
||||
})
|
||||
},
|
||||
).(*cteChange),
|
||||
afterCTE: func(builder *StatementBuilder) {
|
||||
builder.WriteString("UPDATE test SET ")
|
||||
},
|
||||
want: want{
|
||||
stmt: "WITH cte AS (SELECT $1 AS value) UPDATE test SET field = (SELECT * FROM cte WHERE cte.value = table.value)",
|
||||
args: []any{1},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var builder StatementBuilder
|
||||
builder.WriteString("WITH ")
|
||||
if test.prefillBuilder != nil {
|
||||
test.prefillBuilder(&builder)
|
||||
}
|
||||
|
||||
builder.WriteString("cte AS (")
|
||||
test.change.SetName("cte")
|
||||
test.change.WriteCTE(&builder)
|
||||
builder.WriteString(") ")
|
||||
|
||||
if test.afterCTE != nil {
|
||||
test.afterCTE(&builder)
|
||||
}
|
||||
test.change.Write(&builder)
|
||||
|
||||
assert.Equal(t, test.want.stmt, builder.String())
|
||||
assert.Equal(t, builder.Args(), test.want.args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user