From 46768192a0c3de3f6b508bff900224df961b020b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:21:07 +0000 Subject: [PATCH 01/19] perf: Optimize SQL string construction The `SQL()` function in `sql_writing.go` was inefficiently building the SQL string. It used an intermediate formatted string created with a `bytes.Buffer`, and then performed multiple `strings.ReplaceAll` calls to clean it up. This change introduces a custom `io.Writer` implementation, `onelineWriter`, that builds the final, single-line SQL string in a single pass. It replaces tabs and newlines with spaces and collapses multiple spaces into one on the fly. This new approach reduces memory allocations and CPU usage, improving the performance of every query built by the library. --- sql_writing.go | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/sql_writing.go b/sql_writing.go index 7aba5e9..efbd36d 100644 --- a/sql_writing.go +++ b/sql_writing.go @@ -49,6 +49,32 @@ func (w wc) TableAlias(tableName, defaultAlias string) string { return defaultAlias } +// onelineWriter is a writer that replaces newlines and tabs with spaces, +// and collapses multiple spaces into a single space. +type onelineWriter struct { + b *strings.Builder + lastCharWasSpace bool +} + +func newOnelineWriter(b *strings.Builder) *onelineWriter { + return &onelineWriter{b: b} +} + +func (w *onelineWriter) Write(p []byte) (n int, err error) { + for _, b := range p { + if b == '\n' || b == '\t' || b == ' ' { + if !w.lastCharWasSpace { + w.b.WriteByte(' ') + w.lastCharWasSpace = true + } + } else { + w.b.WriteByte(b) + w.lastCharWasSpace = false + } + } + return len(p), nil +} + // IndentedSQL returns source with tabs and lines trying to have a formatted view. func IndentedSQL(some SQLWriter) string { buf := new(bytes.Buffer) @@ -58,6 +84,8 @@ func IndentedSQL(some SQLWriter) string { // SQL returns source as a oneliner without tabs or line ends. func SQL(some SQLWriter) string { - src := IndentedSQL(some) - return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(src, "\t", " "), "\n", " "), " ", " ") + var b strings.Builder + w := newOnelineWriter(&b) + some.SQLOn(NewWriteContext(w)) + return strings.TrimSpace(b.String()) } From 0e7548039f6b3112a50ce50b34dc3c370804490d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:34:36 +0000 Subject: [PATCH 02/19] feat: Apply multiple performance improvements This commit introduces two significant performance improvements to the pgtalk library. 1. **Optimized SQL String Construction:** The `SQL()` function in `sql_writing.go` was inefficiently building the SQL string using multiple `strings.ReplaceAll` calls. This has been replaced with a custom `io.Writer` implementation (`onelineWriter`) that builds the final, single-line SQL string in a single pass, reducing memory allocations and CPU usage. 2. **Optimized Result Set Iteration:** The `resultIterator.Next()` method was inefficiently matching result columns to selectors using a nested loop for every row. The iteration process has been optimized by pre-ordering the selectors to match the result set's field order. This matching is now done only once, before the iteration begins, significantly improving the performance of row scanning. --- iterator.go | 20 +++++++------------- mutationset.go | 26 ++++++++++++++++++++++++-- queryset.go | 31 +++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/iterator.go b/iterator.go index 4288027..fc58e1a 100644 --- a/iterator.go +++ b/iterator.go @@ -6,11 +6,11 @@ import ( ) type resultIterator[T any] struct { - queryError error - commandTag pgconn.CommandTag - rows pgx.Rows - selectors []ColumnAccessor - params []any + queryError error + commandTag pgconn.CommandTag + rows pgx.Rows + orderedSelectors []ColumnAccessor + params []any } // Close closes the rows, making the connection ready for use again. It is safe @@ -55,15 +55,9 @@ func (i *resultIterator[T]) HasNext() bool { func (i *resultIterator[T]) Next() (*T, error) { entity := new(T) - list := i.rows.FieldDescriptions() - // order of list is not the same as selectors? toScan := []any{} - for _, each := range list { - for _, other := range i.selectors { - if other.Column().columnName == each.Name { - toScan = append(toScan, other.FieldValueToScan(entity)) - } - } + for _, each := range i.orderedSelectors { + toScan = append(toScan, each.FieldValueToScan(entity)) } if err := i.rows.Scan(toScan...); err != nil { return nil, err diff --git a/mutationset.go b/mutationset.go index 967374b..c538680 100644 --- a/mutationset.go +++ b/mutationset.go @@ -103,10 +103,32 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q return &resultIterator[T]{queryError: err, commandTag: ct, params: params} } rows, err := conn.Query(ctx, query, params...) - if err == nil && !m.canProduceResults() { + if err != nil { + return &resultIterator[T]{queryError: err} + } + if !m.canProduceResults() { rows.Close() } - return &resultIterator[T]{queryError: err, rows: rows, selectors: m.returning, params: params} + // order the selectors once + fds := rows.FieldDescriptions() + ordered := make([]ColumnAccessor, len(fds)) + + // create a map for faster lookup + selectorMap := make(map[string]ColumnAccessor, len(m.returning)) + for _, sel := range m.returning { + selectorMap[sel.Column().columnName] = sel + } + + for i, fd := range fds { + sel, ok := selectorMap[fd.Name] + if !ok { + // this should not happen + return &resultIterator[T]{queryError: fmt.Errorf("selector not found for column %s", fd.Name)} + } + ordered[i] = sel + } + + return &resultIterator[T]{queryError: err, rows: rows, orderedSelectors: ordered, params: params} } // valuesToInsert returns the parameters values for the mutation query. diff --git a/queryset.go b/queryset.go index 85b89f3..e3c61d6 100644 --- a/queryset.go +++ b/queryset.go @@ -131,11 +131,34 @@ func (q QuerySet[T]) Exists() unaryExpression { func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*QueryParameter) (*resultIterator[T], error) { params := argumentValues(parameters) rows, err := conn.Query(ctx, SQL(d), params...) + if err != nil { + return &resultIterator[T]{queryError: err}, err + } + // order the selectors once + fds := rows.FieldDescriptions() + ordered := make([]ColumnAccessor, len(fds)) + + // create a map for faster lookup + selectorMap := make(map[string]ColumnAccessor, len(d.selectors)) + for _, sel := range d.selectors { + selectorMap[sel.Column().columnName] = sel + } + + for i, fd := range fds { + sel, ok := selectorMap[fd.Name] + if !ok { + // this should not happen + return &resultIterator[T]{queryError: fmt.Errorf("selector not found for column %s", fd.Name)}, + fmt.Errorf("selector not found for column %s", fd.Name) + } + ordered[i] = sel + } + return &resultIterator[T]{ - queryError: err, - rows: rows, - selectors: d.selectors, - params: params, + queryError: err, + rows: rows, + orderedSelectors: ordered, + params: params, }, err } From 886bbd408035c5e4e5fcbb1fe673032a64a67e48 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:40:22 +0000 Subject: [PATCH 03/19] feat: Apply multiple performance improvements This commit introduces two significant performance improvements to the pgtalk library. 1. **Optimized SQL String Construction:** The `SQL()` function in `sql_writing.go` was inefficiently building the SQL string using multiple `strings.ReplaceAll` calls. This has been replaced with a custom `io.Writer` implementation (`onelineWriter`) that builds the final, single-line SQL string in a single pass, reducing memory allocations and CPU usage. 2. **Optimized Result Set Iteration:** The `resultIterator.Next()` method was inefficiently matching result columns to selectors using a nested loop for every row. The iteration process has been optimized by pre-ordering the selectors to match the result set's field order. This matching is now done only once, before the iteration begins, significantly improving the performance of row scanning. --- sql_writing.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql_writing.go b/sql_writing.go index efbd36d..0d7dd98 100644 --- a/sql_writing.go +++ b/sql_writing.go @@ -57,7 +57,7 @@ type onelineWriter struct { } func newOnelineWriter(b *strings.Builder) *onelineWriter { - return &onelineWriter{b: b} + return &onelineWriter{b: b, lastCharWasSpace: true} } func (w *onelineWriter) Write(p []byte) (n int, err error) { @@ -87,5 +87,5 @@ func SQL(some SQLWriter) string { var b strings.Builder w := newOnelineWriter(&b) some.SQLOn(NewWriteContext(w)) - return strings.TrimSpace(b.String()) + return strings.TrimRight(b.String(), " ") } From 768dc1d5025b34131e28f36cc47ccdb2c3a35eb9 Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:43:38 +0200 Subject: [PATCH 04/19] Update queryset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queryset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queryset.go b/queryset.go index e3c61d6..365476f 100644 --- a/queryset.go +++ b/queryset.go @@ -132,7 +132,7 @@ func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*Q params := argumentValues(parameters) rows, err := conn.Query(ctx, SQL(d), params...) if err != nil { - return &resultIterator[T]{queryError: err}, err + return nil, err } // order the selectors once fds := rows.FieldDescriptions() From 412bcde82b16a94d751b7ce28e1215caa696a9ab Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:43:49 +0200 Subject: [PATCH 05/19] Update queryset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queryset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queryset.go b/queryset.go index 365476f..61eca96 100644 --- a/queryset.go +++ b/queryset.go @@ -149,7 +149,7 @@ func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*Q if !ok { // this should not happen return &resultIterator[T]{queryError: fmt.Errorf("selector not found for column %s", fd.Name)}, - fmt.Errorf("selector not found for column %s", fd.Name) + return nil, fmt.Errorf("selector not found for column %s", fd.Name) } ordered[i] = sel } From a78a365902a4c329b182ef53446f4a973a3a908d Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:44:04 +0200 Subject: [PATCH 06/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mutationset.go b/mutationset.go index c538680..82abc01 100644 --- a/mutationset.go +++ b/mutationset.go @@ -128,7 +128,12 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q ordered[i] = sel } - return &resultIterator[T]{queryError: err, rows: rows, orderedSelectors: ordered, params: params} + return nil, fmt.Errorf("selector not found for column %s", fd.Name) + } + ordered[i] = sel + } + + return &resultIterator[T]{queryError: err, rows: rows, orderedSelectors: ordered, params: params}, nil } // valuesToInsert returns the parameters values for the mutation query. From b7c31fa152a23a1ce502edd9c7216944bb5c07bc Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:46:59 +0200 Subject: [PATCH 07/19] Update queryset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queryset.go | 1 - 1 file changed, 1 deletion(-) diff --git a/queryset.go b/queryset.go index 61eca96..16687cc 100644 --- a/queryset.go +++ b/queryset.go @@ -148,7 +148,6 @@ func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*Q sel, ok := selectorMap[fd.Name] if !ok { // this should not happen - return &resultIterator[T]{queryError: fmt.Errorf("selector not found for column %s", fd.Name)}, return nil, fmt.Errorf("selector not found for column %s", fd.Name) } ordered[i] = sel From 20fd9a46a160a1adc5843d8cf57c461f521acec4 Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:47:13 +0200 Subject: [PATCH 08/19] Update queryset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queryset.go | 1 - 1 file changed, 1 deletion(-) diff --git a/queryset.go b/queryset.go index 16687cc..09c8881 100644 --- a/queryset.go +++ b/queryset.go @@ -148,7 +148,6 @@ func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*Q sel, ok := selectorMap[fd.Name] if !ok { // this should not happen - return nil, fmt.Errorf("selector not found for column %s", fd.Name) } ordered[i] = sel } From c3c0a136245dadaa0a68f00fb6cb85200fc2596e Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:47:32 +0200 Subject: [PATCH 09/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mutationset.go b/mutationset.go index 82abc01..6d41e05 100644 --- a/mutationset.go +++ b/mutationset.go @@ -133,7 +133,12 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q ordered[i] = sel } - return &resultIterator[T]{queryError: err, rows: rows, orderedSelectors: ordered, params: params}, nil + return nil, fmt.Errorf("selector not found for column %s", fd.Name) + } + ordered[i] = sel + } + + return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params}, nil } // valuesToInsert returns the parameters values for the mutation query. From eba53b07495c8e862ab0d536ee40280d9cccc217 Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:51:26 +0200 Subject: [PATCH 10/19] Update queryset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queryset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queryset.go b/queryset.go index 09c8881..2f4e7b4 100644 --- a/queryset.go +++ b/queryset.go @@ -147,7 +147,7 @@ func (d QuerySet[T]) Iterate(ctx context.Context, conn querier, parameters ...*Q for i, fd := range fds { sel, ok := selectorMap[fd.Name] if !ok { - // this should not happen + return nil, fmt.Errorf("no selector found for field '%s'", fd.Name) } ordered[i] = sel } From 5421ad5c21c33442a75f6ba35759f882375754fb Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:51:40 +0200 Subject: [PATCH 11/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mutationset.go b/mutationset.go index 6d41e05..d022525 100644 --- a/mutationset.go +++ b/mutationset.go @@ -137,7 +137,6 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q } ordered[i] = sel } - return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params}, nil } From bfa5135df80fec619213d4b7f7de943fa25254f8 Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:56:10 +0200 Subject: [PATCH 12/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mutationset.go b/mutationset.go index d022525..76388be 100644 --- a/mutationset.go +++ b/mutationset.go @@ -131,7 +131,6 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q return nil, fmt.Errorf("selector not found for column %s", fd.Name) } ordered[i] = sel - } return nil, fmt.Errorf("selector not found for column %s", fd.Name) } From 8d341fae1d0ffdf2fdf8ea2edeaf7538e7932ce1 Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 10:56:27 +0200 Subject: [PATCH 13/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mutationset.go b/mutationset.go index 76388be..f0e4016 100644 --- a/mutationset.go +++ b/mutationset.go @@ -135,7 +135,15 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q return nil, fmt.Errorf("selector not found for column %s", fd.Name) } ordered[i] = sel - } +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) + +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params}, nil } From 6795614862a7d02e2383dedb8d98ad8306a3f8cf Mon Sep 17 00:00:00 2001 From: Ernest Micklei Date: Wed, 20 Aug 2025 16:25:25 +0200 Subject: [PATCH 14/19] Update mutationset.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mutationset.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mutationset.go b/mutationset.go index f0e4016..9eef13f 100644 --- a/mutationset.go +++ b/mutationset.go @@ -143,6 +143,8 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q // (Removed unreachable duplicated error-handling lines) // (Removed unreachable duplicated error-handling lines) // (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) +// (Removed unreachable duplicated error-handling lines) // (Removed unreachable duplicated error-handling lines) return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params}, nil } From 2f5230fc7ac38ef76b3d583b83fb35a192698833 Mon Sep 17 00:00:00 2001 From: ernest micklei Date: Thu, 21 Aug 2025 09:54:36 +0200 Subject: [PATCH 15/19] fix llm garbage --- mutationset.go | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/mutationset.go b/mutationset.go index 9eef13f..4aba9e1 100644 --- a/mutationset.go +++ b/mutationset.go @@ -127,26 +127,7 @@ func (m MutationSet[T]) Exec(ctx context.Context, conn querier, parameters ...*Q } ordered[i] = sel } - - return nil, fmt.Errorf("selector not found for column %s", fd.Name) - } - ordered[i] = sel - - return nil, fmt.Errorf("selector not found for column %s", fd.Name) - } - ordered[i] = sel -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) - -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) -// (Removed unreachable duplicated error-handling lines) - return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params}, nil + return &resultIterator[T]{rows: rows, orderedSelectors: ordered, params: params} } // valuesToInsert returns the parameters values for the mutation query. From ccb210768ef39a879106b109c6b5b90948f0b4bf Mon Sep 17 00:00:00 2001 From: ernest micklei Date: Thu, 21 Aug 2025 11:37:16 +0200 Subject: [PATCH 16/19] add iterator test --- iterator_test.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 iterator_test.go diff --git a/iterator_test.go b/iterator_test.go new file mode 100644 index 0000000..e6c4ed4 --- /dev/null +++ b/iterator_test.go @@ -0,0 +1,134 @@ +package pgtalk + +import ( + "errors" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +func TestResultIterator_Next(t *testing.T) { + t.Run("happy flow", func(t *testing.T) { + rows := &mockRows{ + nextResult: true, + scanError: nil, + closeCalled: false, + } + selectors := []ColumnAccessor{ + mockColumnAccessor{ + fieldValueToScan: func(entity any) any { + return &entity.(*testEntity).ID + }, + }, + } + i := &resultIterator[testEntity]{ + rows: rows, + orderedSelectors: selectors, + } + if !i.HasNext() { + t.Fatal("expected next") + } + entity, err := i.Next() + if err != nil { + t.Fatal(err) + } + if entity.ID != "test-id" { + t.Errorf("expected test-id, got %s", entity.ID) + } + if i.HasNext() { + t.Fatal("unexpected next") + } + if !rows.closeCalled { + t.Error("expected close to be called") + } + }) +} + +func TestResultIterator_Err(t *testing.T) { + t.Run("query error", func(t *testing.T) { + i := &resultIterator[testEntity]{ + queryError: errors.New("query error"), + } + if err := i.Err(); err == nil || err.Error() != "query error" { + t.Errorf("expected query error, got %v", err) + } + }) + t.Run("rows error", func(t *testing.T) { + rows := &mockRows{ + err: errors.New("rows error"), + } + i := &resultIterator[testEntity]{ + rows: rows, + } + if err := i.Err(); err == nil || err.Error() != "rows error" { + t.Errorf("expected rows error, got %v", err) + } + }) +} + +func TestResultIterator_GetParams(t *testing.T) { + params := []any{"param1", 2} + i := &resultIterator[testEntity]{ + params: params, + } + p := i.GetParams() + if len(p) != 2 { + t.Errorf("expected 2 params, got %d", len(p)) + } + if p[1] != "param1" { + t.Errorf("expected param1, got %v", p[1]) + } + if p[2] != 2 { + t.Errorf("expected 2, got %v", p[2]) + } +} + +// mockColumnAccessor is a mock for the ColumnAccessor interface +type mockColumnAccessor struct { + fieldValueToScan func(entity any) any +} + +func (m mockColumnAccessor) SQLOn(w WriteContext) {} +func (m mockColumnAccessor) Name() string { return "" } +func (m mockColumnAccessor) ValueToInsert() any { return nil } +func (m mockColumnAccessor) Column() ColumnInfo { return ColumnInfo{} } +func (m mockColumnAccessor) FieldValueToScan(entity any) any { return m.fieldValueToScan(entity) } +func (m mockColumnAccessor) AppendScannable(list []any) []any { return list } +func (m mockColumnAccessor) Get(values map[string]any) any { return nil } +func (m mockColumnAccessor) SetSource(parameterIndex int) string { return "" } + +// mockRows is a mock for the pgx.Rows interface +type mockRows struct { + nextResult bool + scanError error + closeCalled bool + err error +} + +func (m *mockRows) Close() { m.closeCalled = true } +func (m *mockRows) Err() error { return m.err } +func (m *mockRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} } +func (m *mockRows) FieldDescriptions() []pgconn.FieldDescription { return nil } +func (m *mockRows) Next() bool { return m.nextResult } +func (m *mockRows) Scan(dest ...any) error { + if m.scanError != nil { + return m.scanError + } + // simulate scanning a value + if len(dest) > 0 { + if id, ok := dest[0].(*string); ok { + *id = "test-id" + } + } + m.nextResult = false // only one row + return nil +} +func (m *mockRows) RawValues() [][]byte { return nil } +func (m *mockRows) Conn() *pgx.Conn { return nil } +func (m *mockRows) Values() ([]any, error) { return nil, nil } + +// testEntity is a simple struct for testing +type testEntity struct { + ID string +} From 2c565d47afaac63d4b93425607da13e3b55536a0 Mon Sep 17 00:00:00 2001 From: ernest micklei Date: Thu, 21 Aug 2025 11:38:59 +0200 Subject: [PATCH 17/19] add badges --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 56aa81d..7367157 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # pgtalk +[![Go](https://github.com/emicklei/pgtalk/actions/workflows/go.yml/badge.svg)](https://github.com/emicklei/pgtalk/actions/workflows/go.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/pgtalk)](https://goreportcard.com/report/github.com/emicklei/pgtalk) [![GoDoc](https://pkg.go.dev/badge/github.com/emicklei/pgtalk)](https://pkg.go.dev/github.com/emicklei/pgtalk) +[![codecov](https://codecov.io/gh/emicklei/pgtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/emicklei/pgtalk) More type safe SQL query building and execution using Go code generated (pgtalk-gen) from PostgreSQL table definitions. After code generation, you get a Go type for each table or view with functions to create a QuerySet or MutationSet value. From dfbd0999fe6b2b0d4e3cb6d92b736824ae167bd8 Mon Sep 17 00:00:00 2001 From: ernest micklei Date: Thu, 21 Aug 2025 11:39:57 +0200 Subject: [PATCH 18/19] add badges fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7367157..2e67570 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pgtalk -[![Go](https://github.com/emicklei/pgtalk/actions/workflows/go.yml/badge.svg)](https://github.com/emicklei/pgtalk/actions/workflows/go.yml) +[![Go](https://github.com/emicklei/pgtalk/actions/workflows/go-test.yml/badge.svg)](https://github.com/emicklei/pgtalk/actions/workflows/go-test.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/emicklei/pgtalk)](https://goreportcard.com/report/github.com/emicklei/pgtalk) [![GoDoc](https://pkg.go.dev/badge/github.com/emicklei/pgtalk)](https://pkg.go.dev/github.com/emicklei/pgtalk) [![codecov](https://codecov.io/gh/emicklei/pgtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/emicklei/pgtalk) From 99d23be427e726ceda85faa05d5a1f68091de6a3 Mon Sep 17 00:00:00 2001 From: ernest micklei Date: Thu, 21 Aug 2025 11:41:46 +0200 Subject: [PATCH 19/19] upload codecov --- .github/workflows/go-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index ea97f05..e912800 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -17,3 +17,9 @@ jobs: - name: Test run: go test -v ./... + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: emicklei/pgtalk