From 4fcc99f48f0ad603e48126c2bb829057a19e243e Mon Sep 17 00:00:00 2001 From: go-jet Date: Thu, 13 Mar 2025 13:05:35 +0100 Subject: [PATCH 1/2] Add support for assigning one ColumnList to another in INSERT and UPDATE queries. --- internal/jet/column_assigment.go | 19 +++------ internal/jet/column_list.go | 57 +++++++++++++++++++++++---- internal/jet/column_list_assigment.go | 21 ++++++++++ internal/jet/column_list_test.go | 25 ++++++++++++ internal/jet/column_types.go | 48 +++++++++++----------- internal/jet/expression.go | 3 ++ tests/postgres/insert_test.go | 41 +++++++++++++++++++ 7 files changed, 170 insertions(+), 44 deletions(-) create mode 100644 internal/jet/column_list_assigment.go create mode 100644 internal/jet/column_list_test.go diff --git a/internal/jet/column_assigment.go b/internal/jet/column_assigment.go index c888433..283afd4 100644 --- a/internal/jet/column_assigment.go +++ b/internal/jet/column_assigment.go @@ -1,27 +1,20 @@ package jet -// ColumnAssigment is interface wrapper around column assigment +// ColumnAssigment is interface wrapper around column assignment type ColumnAssigment interface { Serializer - isColumnAssigment() + isColumnAssignment() } type columnAssigmentImpl struct { - column ColumnSerializer - expression Expression + column ColumnSerializer + toAssign Serializer } -func NewColumnAssignment(serializer ColumnSerializer, expression Expression) ColumnAssigment { - return &columnAssigmentImpl{ - column: serializer, - expression: expression, - } -} - -func (a columnAssigmentImpl) isColumnAssigment() {} +func (a columnAssigmentImpl) isColumnAssignment() {} func (a columnAssigmentImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { a.column.serialize(statement, out, ShortName.WithFallTrough(options)...) out.WriteString("=") - a.expression.serialize(statement, out, FallTrough(options)...) + a.toAssign.serialize(statement, out, FallTrough(options)...) } diff --git a/internal/jet/column_list.go b/internal/jet/column_list.go index 24fe428..2a0d433 100644 --- a/internal/jet/column_list.go +++ b/internal/jet/column_list.go @@ -1,17 +1,60 @@ package jet +import "fmt" + // ColumnList is a helper type to support list of columns as single projection type ColumnList []ColumnExpression -// SET creates column assigment for each column in column list. expression should be created by ROW function +func (cl ColumnList) isExpressionOrColumnList() {} + +// SET creates a column assignment from the current ColumnList using the provided expression. +// This assignment can be used in INSERT queries (e.g., to set columns on conflict) or in UPDATE queries +// (e.g., to assign new values to columns). // -// Link.UPDATE(). -// SET(Link.MutableColumns.SET(ROW(String("github.com"), Bool(false))). -// WHERE(Link.ID.EQ(Int(0))) -func (cl ColumnList) SET(expression Expression) ColumnAssigment { +// The expression can be: +// - Another ColumnList: It must have the same length as the current ColumnList and each column must match by name +// - A ROW expression containing values. +// - A SELECT statement that returns a matching column list structure. +// +// Examples: +// +// Link.AllColumns.SET(ROW(String("github.com"), Bool(false))) +// +// Link.MutableColumns.SET(Link.EXCLUDED.MutableColumns) +// +// Link.MutableColumns.SET( +// SELECT(Link.MutableColumns). +// FROM(Link). +// WHERE(Link.ID.EQ(Int(200))), +// ) +func (cl ColumnList) SET(toAssignExp expressionOrColumnList) ColumnAssigment { + + if toAssign, ok := toAssignExp.(ColumnList); ok { + if len(cl) != len(toAssign) { + panic(fmt.Sprintf("jet: column list length mismatch: expected %d columns, got %d", len(cl), len(toAssign))) + } + + var ret columnListAssigment + + for i, column := range cl { + if column.Name() != toAssign[i].Name() { + panic(fmt.Sprintf("jet: column name mismatch at index %d: expected column '%s', got '%s'", + i, column.Name(), toAssign[i].Name(), + )) + } + + ret = append(ret, columnAssigmentImpl{ + column: column, + toAssign: toAssign[i], + }) + } + + return ret + } + return columnAssigmentImpl{ - column: cl, - expression: expression, + column: cl, + toAssign: toAssignExp, } } diff --git a/internal/jet/column_list_assigment.go b/internal/jet/column_list_assigment.go new file mode 100644 index 0000000..aa66690 --- /dev/null +++ b/internal/jet/column_list_assigment.go @@ -0,0 +1,21 @@ +package jet + +type expressionOrColumnList interface { + Serializer + isExpressionOrColumnList() +} + +type columnListAssigment []ColumnAssigment + +func (c columnListAssigment) isColumnAssignment() {} + +func (c columnListAssigment) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + for i, columnAssigment := range c { + if i > 0 { + out.WriteString(",") + out.NewLine() + } + + columnAssigment.serialize(statement, out, options...) + } +} diff --git a/internal/jet/column_list_test.go b/internal/jet/column_list_test.go new file mode 100644 index 0000000..f026370 --- /dev/null +++ b/internal/jet/column_list_test.go @@ -0,0 +1,25 @@ +package jet + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestColumnList_SET(t *testing.T) { + columnList1 := ColumnList{IntegerColumn("id"), StringColumn("Name"), BoolColumn("active")} + columnList2 := ColumnList{IntegerColumn("id"), StringColumn("Name"), BoolColumn("active")} + + columnList1.SET(columnList2) + + columnList3 := ColumnList{IntegerColumn("id"), StringColumn("Name")} + + require.PanicsWithValue(t, "jet: column list length mismatch: expected 2 columns, got 3", func() { + columnList3.SET(columnList1) + }) + + columnList4 := ColumnList{IntegerColumn("id"), StringColumn("FullName"), BoolColumn("active")} + + require.PanicsWithValue(t, "jet: column name mismatch at index 1: expected column 'Name', got 'FullName'", func() { + columnList1.SET(columnList4) + }) +} diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index cec0530..68a5042 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -28,8 +28,8 @@ func (i *boolColumnImpl) From(subQuery SelectTable) ColumnBool { func (i *boolColumnImpl) SET(boolExp BoolExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: boolExp, + column: i, + toAssign: boolExp, } } @@ -72,8 +72,8 @@ func (i *floatColumnImpl) From(subQuery SelectTable) ColumnFloat { func (i *floatColumnImpl) SET(floatExp FloatExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: floatExp, + column: i, + toAssign: floatExp, } } @@ -117,8 +117,8 @@ func (i *integerColumnImpl) From(subQuery SelectTable) ColumnInteger { func (i *integerColumnImpl) SET(intExp IntegerExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: intExp, + column: i, + toAssign: intExp, } } @@ -163,8 +163,8 @@ func (i *stringColumnImpl) From(subQuery SelectTable) ColumnString { func (i *stringColumnImpl) SET(stringExp StringExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: stringExp, + column: i, + toAssign: stringExp, } } @@ -208,8 +208,8 @@ func (i *blobColumnImpl) From(subQuery SelectTable) ColumnBlob { func (i *blobColumnImpl) SET(blobExp BlobExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: blobExp, + column: i, + toAssign: blobExp, } } @@ -252,8 +252,8 @@ func (i *timeColumnImpl) From(subQuery SelectTable) ColumnTime { func (i *timeColumnImpl) SET(timeExp TimeExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: timeExp, + column: i, + toAssign: timeExp, } } @@ -295,8 +295,8 @@ func (i *timezColumnImpl) From(subQuery SelectTable) ColumnTimez { func (i *timezColumnImpl) SET(timezExp TimezExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: timezExp, + column: i, + toAssign: timezExp, } } @@ -339,8 +339,8 @@ func (i *timestampColumnImpl) From(subQuery SelectTable) ColumnTimestamp { func (i *timestampColumnImpl) SET(timestampExp TimestampExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: timestampExp, + column: i, + toAssign: timestampExp, } } @@ -383,8 +383,8 @@ func (i *timestampzColumnImpl) From(subQuery SelectTable) ColumnTimestampz { func (i *timestampzColumnImpl) SET(timestampzExp TimestampzExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: timestampzExp, + column: i, + toAssign: timestampzExp, } } @@ -427,8 +427,8 @@ func (i *dateColumnImpl) From(subQuery SelectTable) ColumnDate { func (i *dateColumnImpl) SET(dateExp DateExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: dateExp, + column: i, + toAssign: dateExp, } } @@ -460,8 +460,8 @@ type intervalColumnImpl struct { func (i *intervalColumnImpl) SET(intervalExp IntervalExpression) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: intervalExp, + column: i, + toAssign: intervalExp, } } @@ -516,8 +516,8 @@ func (i *rangeColumnImpl[T]) From(subQuery SelectTable) ColumnRange[T] { func (i *rangeColumnImpl[T]) SET(rangeExp Range[T]) ColumnAssigment { return columnAssigmentImpl{ - column: i, - expression: rangeExp, + column: i, + toAssign: rangeExp, } } diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 42b63f2..62e5aff 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -9,6 +9,7 @@ type Expression interface { Projection GroupByClause OrderByClause + expressionOrColumnList serializeForJsonValue(statement StatementType, out *SQLBuilder) setRoot(root Expression) @@ -37,6 +38,8 @@ type ExpressionInterfaceImpl struct { Root Expression } +func (e *ExpressionInterfaceImpl) isExpressionOrColumnList() {} + func (e *ExpressionInterfaceImpl) setRoot(root Expression) { e.Root = root } diff --git a/tests/postgres/insert_test.go b/tests/postgres/insert_test.go index c375396..f6083e9 100644 --- a/tests/postgres/insert_test.go +++ b/tests/postgres/insert_test.go @@ -196,6 +196,47 @@ RETURNING link.id AS "link.id", testutils.AssertExecAndRollback(t, stmt, db, 2) }) + t.Run("do update column list", func(t *testing.T) { + stmt := Link.INSERT(). + VALUES(1, "http://www.postgresqltutorial.com", "PostgreSQL Tutorial", DEFAULT). + ON_CONFLICT(Link.ID).DO_UPDATE( + SET( + Link.MutableColumns.SET(Link.EXCLUDED.MutableColumns), + ), + ).RETURNING(Link.AllColumns) + + testutils.AssertDebugStatementSql(t, stmt, ` +INSERT INTO test_sample.link +VALUES (1, 'http://www.postgresqltutorial.com', 'PostgreSQL Tutorial', DEFAULT) +ON CONFLICT (id) DO UPDATE + SET url = excluded.url, + name = excluded.name, + description = excluded.description +RETURNING link.id AS "link.id", + link.url AS "link.url", + link.name AS "link.name", + link.description AS "link.description"; +`) + + testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) { + var dest []model.Link + + err := stmt.QueryContext(ctx, tx, &dest) + require.NoError(t, err) + + testutils.AssertJSON(t, dest, ` +[ + { + "ID": 1, + "URL": "http://www.postgresqltutorial.com", + "Name": "PostgreSQL Tutorial", + "Description": null + } +] +`) + }) + }) + t.Run("do update complex", func(t *testing.T) { skipForCockroachDB(t) // does not support ROW From 34ee39ca2a0c7c3d1837e263d217abf71bb5447c Mon Sep 17 00:00:00 2001 From: go-jet Date: Thu, 13 Mar 2025 13:37:27 +0100 Subject: [PATCH 2/2] Upgrade jet generator version. --- cmd/jet/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/jet/version.go b/cmd/jet/version.go index f5371e4..6430eba 100644 --- a/cmd/jet/version.go +++ b/cmd/jet/version.go @@ -1,3 +1,3 @@ package main -const version = "v2.12.0" +const version = "v2.13.0"