Skip to content

Fix double-encoding of PostgreSQL json/jsonb columns; add step.json_parse#297

Merged
intel352 merged 5 commits intomainfrom
copilot/fix-double-encoding-jsonb-types
Mar 12, 2026
Merged

Fix double-encoding of PostgreSQL json/jsonb columns; add step.json_parse#297
intel352 merged 5 commits intomainfrom
copilot/fix-double-encoding-jsonb-types

Conversation

Copy link
Contributor

Copilot AI commented Mar 11, 2026

The pgx driver returns json/jsonb columns as raw []byte JSON text. All three row-scanning sites (DBQueryStep, DBQueryCachedStep, WorkflowDatabase.Query) converted []bytestring, making these values opaque strings that downstream steps (step.json_response, step.jq, step.transform) would JSON-encode a second time.

Changes

  • parseJSONBytesOrString helper (module/pipeline_step_db_query.go) — attempts json.Unmarshal on any []byte column value; falls back to string(b) for non-JSON bytes (e.g. bytea). Idempotent and driver-agnostic.
  • Applied to all three scannersDBQueryStep, DBQueryCachedStep, and WorkflowDatabase.Query all use the same helper, so json/jsonb values arrive in the pipeline context as map[string]any / []any rather than strings.
  • New step.json_parse — explicit escape hatch for pipelines that source JSON strings from non-DB steps (HTTP calls, legacy drivers, text columns). Parses a string or []byte value at a dot-path into a structured object; pass-through for already-parsed values.
# Explicit parsing when needed (e.g. text column containing JSON)
- name: parse_data
  type: step.json_parse
  config:
    source: steps.fetch.row.json_column  # JSON string → map/slice
    target: parsed_data                  # default: "value"

- name: respond
  type: step.json_response
  config:
    body:
      data:
        _from: steps.parse_data.parsed_data

For json/jsonb columns via pgx no YAML change is needed — step.db_query now delivers structured values automatically.

Original prompt

This section details on the original issue you should resolve

<issue_title>step.json_response: double-encoding of PostgreSQL json/jsonb types via pgx driver</issue_title>
<issue_description>## Problem

When step.db_query returns columns of PostgreSQL json or jsonb type, the pgx driver returns them as raw JSON strings (e.g., "[{\"id\": 1}]"), not parsed Go maps/slices. When step.json_response with _from copies these values into the response body, they are treated as strings and JSON-encoded again, resulting in double-encoded output:

{
  "data": "[{\"id\": 1, \"type\": \"follow-ups\"}]"
}

Instead of:

{
  "data": [{"id": 1, "type": "follow-ups"}]
}

This affects any pipeline that constructs complex JSON structures in PostgreSQL (using json_build_object, json_agg, etc.) and tries to return them via step.json_response.

Use Case

APIs that need to return structured responses (e.g., JSON:API format with nested data, included, meta objects) where the response structure is built in SQL for efficiency. This is common when:

  • The response format requires joins, aggregations, and nested relationships
  • Building the structure in SQL avoids N+1 query patterns
  • The response includes pagination metadata computed from the same query

Example: a follow-ups endpoint returning JSON:API format with relationships and included resources, all computed in a single SQL query using json_build_object and json_agg.

Proposed Solution

Option A: JSON-aware _from resolution

When step.json_response resolves a _from value, detect if the value is a string that looks like JSON (starts with { or [) and parse it before embedding. This could be opt-in:

- name: respond
  type: step.json_response
  config:
    status: 200
    body:
      _from: steps.fetch.row.json_column
      _from_raw: true    # treat the value as pre-serialized JSON, not a string

Option B: step.raw_response with body_from (already exists)

step.raw_response with body_from works as a complete workaround when the entire response body is a single JSON column cast to ::text. However, this only works for the whole response — you can't compose a response from multiple _from fields that are individually JSON-typed.

Option C: step.json_parse intermediate step

A step that parses a JSON string value back into a structured object in the pipeline context, so downstream steps see it as a map/slice rather than a string:

- name: parse_json
  type: step.json_parse
  config:
    source: steps.fetch.row.json_column
    target: parsed_data

- name: respond
  type: step.json_response
  config:
    body:
      data:
        _from: steps.parse_json.parsed_data

Current Workaround

Cast the entire response to ::text in PostgreSQL and use step.raw_response with body_from:

# SQL: SELECT json_build_object('data', ..., 'meta', ...)::text AS response
- name: respond
  type: step.raw_response
  config:
    status: 200
    content_type: application/json
    body_from: steps.fetch.row.response

This works for simple cases but forces the entire response to be a single SQL column and prevents composing responses from multiple pipeline steps or mixing SQL-generated JSON with static fields.</issue_description>

Comments on the Issue (you are @copilot in this section)

@intel352 **Additional context**: `step.transform` (Go templates) and `step.jq` could both serve as response mappers to build JSON:API / HAL / custom envelope formats from flat DB rows — but they're blocked by this same issue. When `step.db_query` returns rows with json/jsonb columns, the values arrive as strings in the pipeline context. `step.transform` templates and `step.jq` expressions then treat them as strings rather than objects, making it impossible to compose structured responses from DB-generated JSON fragments.

Resolving this would unlock using step.jq or step.transform to handle response shaping entirely in YAML, eliminating the need to push JSON envelope construction into SQL with json_build_object.</comment_new>
<comment_new>@intel352
Partial workaround via foreach + transform: For responses built from scalar DB columns (strings, ints, timestamps), step.foreach + step.transform can reshape rows without hitting the double-encoding issue:

- name: fetch
  type: step.db_query
  config:
    database: db
    query: SELECT id, status, texter_id, scheduled_for FROM follow_ups WHERE ...

- name: reshape
  type: step.foreach
  config:
    collection: "{{.steps.fetch.rows}}"
    item_var: row
    step:
      type: step.transform
      config:
        template: |
          {"type": "follow-ups", "id": "{{.row.id}}", "attributes": {"status": "{{.row.status}}"}}

- name: respond
  type: step.json_response
  config:
    body:
      data:
        _from: steps.reshape.results

This...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix double-encoding of PostgreSQL json/jsonb types via pgx driver Fix double-encoding of PostgreSQL json/jsonb columns; add step.json_parse Mar 11, 2026
@intel352 intel352 marked this pull request as ready for review March 11, 2026 23:14
Copilot AI review requested due to automatic review settings March 11, 2026 23:14
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes PostgreSQL json/jsonb values being double-encoded by ensuring DB row scanners parse JSON []byte into structured Go values, and adds a step.json_parse escape hatch for non-DB JSON strings.

Changes:

  • Added parseJSONBytesOrString and applied it across DB row scanning (step.db_query, step.db_query_cached, and WorkflowDatabase.Query) to avoid returning JSON as opaque strings.
  • Introduced step.json_parse (with tests) to explicitly parse JSON strings/bytes at a dot-path into structured values.
  • Registered the new step in the pipelinesteps plugin and updated documentation.

Reviewed changes

Copilot reviewed 9 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
module/pipeline_step_db_query.go Parses []byte columns as JSON when possible; adds shared helper.
module/pipeline_step_db_query_cached.go Uses the shared helper for []byte columns.
module/database.go Uses the shared helper so WorkflowDatabase.Query returns structured JSON too.
module/pipeline_step_db_query_test.go Adds unit tests for parseJSONBytesOrString.
module/pipeline_step_json_parse.go New step.json_parse implementation.
module/pipeline_step_json_parse_test.go Unit tests for step.json_parse.
plugins/pipelinesteps/plugin.go Registers step.json_parse factory/type.
plugins/pipelinesteps/plugin_test.go Ensures the step factory list includes step.json_parse.
DOCUMENTATION.md Documents step.json_parse in the step table.
module/scan_provider_test.go Formatting-only changes (gofmt).
module/platform_do_database.go Comment alignment/formatting (gofmt).
module/platform_do_database_test.go Formatting-only changes (gofmt).

// Execute resolves the source path, parses the value as JSON if it is a string,
// and stores the result under the configured target key.
func (s *JSONParseStep) Execute(_ context.Context, pc *PipelineContext) (*StepResult, error) {
raw := resolveBodyFrom(s.source, pc)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json_parse resolves the source via resolveBodyFrom, which returns nil for missing/unresolvable paths. That means a typo in source will silently produce {target: nil} and the step will appear to succeed. Consider using a strict resolver (e.g. build a data map like JQStep.resolveInput and call resolveDottedPath) or otherwise detecting “path not found” and returning an error so misconfigurations fail fast.

Suggested change
raw := resolveBodyFrom(s.source, pc)
raw := resolveBodyFrom(s.source, pc)
if raw == nil {
return nil, fmt.Errorf("json_parse step %q: source %q not found or resolved to nil", s.name, s.source)
}

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +246
func parseJSONBytesOrString(b []byte) any {
if len(b) == 0 {
return string(b)
}
var v any
if err := json.Unmarshal(b, &v); err == nil {
return v
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseJSONBytesOrString attempts json.Unmarshal on every non-empty []byte column. For large bytea/blob fields that happen to be mostly text, json.Unmarshal can scan a lot of data before failing and add avoidable CPU overhead. Consider a cheap pre-check before unmarshalling (e.g. trim leading whitespace and require the first non-space byte to be one of {, [, ", t, f, n, -, or a digit; optionally also require utf8.Valid) so non-JSON byte slices skip the full JSON parser.

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +249
// parseJSONBytesOrString attempts to unmarshal b as JSON. If successful the
// parsed Go value is returned (map[string]any, []any, string, float64, bool,
// or nil). This transparently handles PostgreSQL json/jsonb columns, which the
// pgx driver delivers as raw JSON bytes rather than pre-typed Go values.
//
// If b is not valid JSON (e.g. PostgreSQL bytea binary data), string(b) is
// returned so that the existing string-fallback behaviour is preserved.
func parseJSONBytesOrString(b []byte) any {
if len(b) == 0 {
return string(b)
}
var v any
if err := json.Unmarshal(b, &v); err == nil {
return v
}
return string(b)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseJSONBytesOrString is defined in pipeline_step_db_query.go but is now used by WorkflowDatabase.Query and DBQueryCachedStep as well. To avoid coupling core database code to a pipeline-step file (and to make the helper easier to discover/reuse), consider moving this helper to a shared location (e.g. module/database_scan_helpers.go or similar) and keep the pipeline steps calling into that shared helper.

Copilot uses AI. Check for mistakes.
@intel352
Copy link
Contributor

@copilot apply changes based on the comments in this thread

Build failing, lint failing, test failing, validation failing

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 12, 2026

@copilot apply changes based on the comments in this thread

Build failing, lint failing, test fail...

All three reviewer suggestions applied in 50bca53:

  1. parseJSONBytesOrString moved to module/database_scan_helpers.go — no longer coupled to the pipeline-step file, all three callers (DBQueryStep, DBQueryCachedStep, WorkflowDatabase.Query) pick it up from the shared location.

  2. Leading-byte pre-check added — trims whitespace then tests the first byte against the set of valid JSON-start characters ({, [, ", t, f, n, -, 0–9). Binary blobs skip json.Unmarshal entirely.

  3. Nil-source error in step.json_parseExecute now returns an error immediately when resolveBodyFrom returns nil, so a typo in source fails fast instead of silently producing {target: nil}. A new TestJSONParseStep_UnresolvablePath test covers this case.

@github-actions
Copy link

github-actions bot commented Mar 12, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:244: parsing iteration count: invalid syntax
baseline-bench.txt:299609: parsing iteration count: invalid syntax
baseline-bench.txt:620730: parsing iteration count: invalid syntax
baseline-bench.txt:934018: parsing iteration count: invalid syntax
baseline-bench.txt:1225997: parsing iteration count: invalid syntax
baseline-bench.txt:1521159: parsing iteration count: invalid syntax
benchmark-results.txt:244: parsing iteration count: invalid syntax
benchmark-results.txt:344234: parsing iteration count: invalid syntax
benchmark-results.txt:684528: parsing iteration count: invalid syntax
benchmark-results.txt:1073638: parsing iteration count: invalid syntax
benchmark-results.txt:1433819: parsing iteration count: invalid syntax
benchmark-results.txt:1832979: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │
                            │       sec/op       │
InterpreterCreation-4               7.477m ± 60%
ComponentLoad-4                     3.477m ±  5%
ComponentExecute-4                  2.035µ ±  2%
PoolContention/workers-1-4          1.097µ ±  4%
PoolContention/workers-2-4          1.117µ ±  5%
PoolContention/workers-4-4          1.087µ ±  1%
PoolContention/workers-8-4          1.091µ ±  4%
PoolContention/workers-16-4         1.100µ ±  2%
ComponentLifecycle-4                3.536m ±  1%
SourceValidation-4                  2.341µ ±  2%
RegistryConcurrent-4                757.6n ±  1%
LoaderLoadFromString-4              3.566m ±  2%
geomean                             18.75µ

                            │ baseline-bench.txt │
                            │        B/op        │
InterpreterCreation-4               1.944Mi ± 0%
ComponentLoad-4                     2.097Mi ± 0%
ComponentExecute-4                  1.203Ki ± 0%
PoolContention/workers-1-4          1.203Ki ± 0%
PoolContention/workers-2-4          1.203Ki ± 0%
PoolContention/workers-4-4          1.203Ki ± 0%
PoolContention/workers-8-4          1.203Ki ± 0%
PoolContention/workers-16-4         1.203Ki ± 0%
ComponentLifecycle-4                2.099Mi ± 0%
SourceValidation-4                  1.984Ki ± 0%
RegistryConcurrent-4                1.133Ki ± 0%
LoaderLoadFromString-4              2.099Mi ± 0%
geomean                             15.05Ki

                            │ baseline-bench.txt │
                            │     allocs/op      │
InterpreterCreation-4                15.09k ± 0%
ComponentLoad-4                      17.43k ± 0%
ComponentExecute-4                    25.00 ± 0%
PoolContention/workers-1-4            25.00 ± 0%
PoolContention/workers-2-4            25.00 ± 0%
PoolContention/workers-4-4            25.00 ± 0%
PoolContention/workers-8-4            25.00 ± 0%
PoolContention/workers-16-4           25.00 ± 0%
ComponentLifecycle-4                 17.48k ± 0%
SourceValidation-4                    32.00 ± 0%
RegistryConcurrent-4                  2.000 ± 0%
LoaderLoadFromString-4               17.47k ± 0%
geomean                               181.2

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                            │ benchmark-results.txt │
                            │        sec/op         │
InterpreterCreation-4                  5.636m ± 70%
ComponentLoad-4                        3.270m ±  6%
ComponentExecute-4                     1.877µ ±  2%
PoolContention/workers-1-4             1.185µ ±  2%
PoolContention/workers-2-4             1.181µ ±  0%
PoolContention/workers-4-4             1.182µ ±  1%
PoolContention/workers-8-4             1.192µ ±  0%
PoolContention/workers-16-4            1.208µ ±  2%
ComponentLifecycle-4                   3.304m ±  1%
SourceValidation-4                     2.179µ ±  1%
RegistryConcurrent-4                   922.4n ±  5%
LoaderLoadFromString-4                 3.341m ±  0%
geomean                                18.70µ

                            │ benchmark-results.txt │
                            │         B/op          │
InterpreterCreation-4                  1.944Mi ± 0%
ComponentLoad-4                        2.097Mi ± 0%
ComponentExecute-4                     1.203Ki ± 0%
PoolContention/workers-1-4             1.203Ki ± 0%
PoolContention/workers-2-4             1.203Ki ± 0%
PoolContention/workers-4-4             1.203Ki ± 0%
PoolContention/workers-8-4             1.203Ki ± 0%
PoolContention/workers-16-4            1.203Ki ± 0%
ComponentLifecycle-4                   2.099Mi ± 0%
SourceValidation-4                     1.984Ki ± 0%
RegistryConcurrent-4                   1.133Ki ± 0%
LoaderLoadFromString-4                 2.099Mi ± 0%
geomean                                15.05Ki

                            │ benchmark-results.txt │
                            │       allocs/op       │
InterpreterCreation-4                   15.09k ± 0%
ComponentLoad-4                         17.43k ± 0%
ComponentExecute-4                       25.00 ± 0%
PoolContention/workers-1-4               25.00 ± 0%
PoolContention/workers-2-4               25.00 ± 0%
PoolContention/workers-4-4               25.00 ± 0%
PoolContention/workers-8-4               25.00 ± 0%
PoolContention/workers-16-4              25.00 ± 0%
ComponentLifecycle-4                    17.48k ± 0%
SourceValidation-4                       32.00 ± 0%
RegistryConcurrent-4                     2.000 ± 0%
LoaderLoadFromString-4                  17.47k ± 0%
geomean                                  181.2

pkg: github.com/GoCodeAlone/workflow/middleware
cpu: AMD EPYC 7763 64-Core Processor                
                                  │ baseline-bench.txt │
                                  │       sec/op       │
CircuitBreakerDetection-4                  297.2n ± 2%
CircuitBreakerExecution_Success-4          22.53n ± 1%
CircuitBreakerExecution_Failure-4          64.51n ± 0%
geomean                                    75.59n

                                  │ baseline-bench.txt │
                                  │        B/op        │
CircuitBreakerDetection-4                 144.0 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │
                                  │     allocs/op      │
CircuitBreakerDetection-4                 1.000 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                  │ benchmark-results.txt │
                                  │        sec/op         │
CircuitBreakerDetection-4                     448.8n ± 2%
CircuitBreakerExecution_Success-4             59.70n ± 0%
CircuitBreakerExecution_Failure-4             64.61n ± 0%
geomean                                       120.1n

                                  │ benchmark-results.txt │
                                  │         B/op          │
CircuitBreakerDetection-4                    144.0 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

                                  │ benchmark-results.txt │
                                  │       allocs/op       │
CircuitBreakerDetection-4                    1.000 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
cpu: AMD EPYC 7763 64-Core Processor                
                                 │ baseline-bench.txt │
                                 │       sec/op       │
JQTransform_Simple-4                     919.0n ± 24%
JQTransform_ObjectConstruction-4         1.498µ ±  2%
JQTransform_ArraySelect-4                3.466µ ±  1%
JQTransform_Complex-4                    39.32µ ±  3%
JQTransform_Throughput-4                 1.818µ ±  1%
SSEPublishDelivery-4                     65.70n ±  1%
geomean                                  1.679µ

                                 │ baseline-bench.txt │
                                 │        B/op        │
JQTransform_Simple-4                   1.273Ki ± 0%
JQTransform_ObjectConstruction-4       1.773Ki ± 0%
JQTransform_ArraySelect-4              2.625Ki ± 0%
JQTransform_Complex-4                  16.22Ki ± 0%
JQTransform_Throughput-4               1.984Ki ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │
                                 │     allocs/op      │
JQTransform_Simple-4                     10.00 ± 0%
JQTransform_ObjectConstruction-4         15.00 ± 0%
JQTransform_ArraySelect-4                30.00 ± 0%
JQTransform_Complex-4                    324.0 ± 0%
JQTransform_Throughput-4                 17.00 ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                 │ benchmark-results.txt │
                                 │        sec/op         │
JQTransform_Simple-4                        949.6n ± 16%
JQTransform_ObjectConstruction-4            1.447µ ±  1%
JQTransform_ArraySelect-4                   3.126µ ±  0%
JQTransform_Complex-4                       34.63µ ±  0%
JQTransform_Throughput-4                    1.757µ ±  0%
SSEPublishDelivery-4                        72.50n ±  1%
geomean                                     1.633µ

                                 │ benchmark-results.txt │
                                 │         B/op          │
JQTransform_Simple-4                      1.273Ki ± 0%
JQTransform_ObjectConstruction-4          1.773Ki ± 0%
JQTransform_ArraySelect-4                 2.625Ki ± 0%
JQTransform_Complex-4                     16.22Ki ± 0%
JQTransform_Throughput-4                  1.984Ki ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                 │ benchmark-results.txt │
                                 │       allocs/op       │
JQTransform_Simple-4                        10.00 ± 0%
JQTransform_ObjectConstruction-4            15.00 ± 0%
JQTransform_ArraySelect-4                   30.00 ± 0%
JQTransform_Complex-4                       324.0 ± 0%
JQTransform_Throughput-4                    17.00 ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
cpu: AMD EPYC 7763 64-Core Processor                
                                    │ baseline-bench.txt │
                                    │       sec/op       │
SchemaValidation_Simple-4                    1.134µ ± 5%
SchemaValidation_AllFields-4                 1.693µ ± 2%
SchemaValidation_FormatValidation-4          1.592µ ± 2%
SchemaValidation_ManySchemas-4               1.851µ ± 2%
geomean                                      1.542µ

                                    │ baseline-bench.txt │
                                    │        B/op        │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │
                                    │     allocs/op      │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                    │ benchmark-results.txt │
                                    │        sec/op         │
SchemaValidation_Simple-4                      1.027µ ± 17%
SchemaValidation_AllFields-4                   1.523µ ±  4%
SchemaValidation_FormatValidation-4            1.460µ ±  1%
SchemaValidation_ManySchemas-4                 1.510µ ±  3%
geomean                                        1.363µ

                                    │ benchmark-results.txt │
                                    │         B/op          │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

                                    │ benchmark-results.txt │
                                    │       allocs/op       │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
cpu: AMD EPYC 7763 64-Core Processor                
                                   │ baseline-bench.txt │
                                   │       sec/op       │
EventStoreAppend_InMemory-4                1.252µ ± 18%
EventStoreAppend_SQLite-4                  1.259m ±  5%
GetTimeline_InMemory/events-10-4           14.37µ ±  4%
GetTimeline_InMemory/events-50-4           81.05µ ± 20%
GetTimeline_InMemory/events-100-4          132.7µ ±  1%
GetTimeline_InMemory/events-500-4          677.3µ ±  1%
GetTimeline_InMemory/events-1000-4         1.399m ±  4%
GetTimeline_SQLite/events-10-4             112.9µ ±  1%
GetTimeline_SQLite/events-50-4             263.2µ ±  2%
GetTimeline_SQLite/events-100-4            445.8µ ±  2%
GetTimeline_SQLite/events-500-4            1.881m ±  8%
GetTimeline_SQLite/events-1000-4           3.736m ±  2%
geomean                                    229.6µ

                                   │ baseline-bench.txt │
                                   │        B/op        │
EventStoreAppend_InMemory-4                 760.0 ± 10%
EventStoreAppend_SQLite-4                 1.984Ki ±  1%
GetTimeline_InMemory/events-10-4          7.953Ki ±  0%
GetTimeline_InMemory/events-50-4          46.62Ki ±  0%
GetTimeline_InMemory/events-100-4         94.48Ki ±  0%
GetTimeline_InMemory/events-500-4         472.8Ki ±  0%
GetTimeline_InMemory/events-1000-4        944.3Ki ±  0%
GetTimeline_SQLite/events-10-4            16.74Ki ±  0%
GetTimeline_SQLite/events-50-4            87.14Ki ±  0%
GetTimeline_SQLite/events-100-4           175.4Ki ±  0%
GetTimeline_SQLite/events-500-4           846.1Ki ±  0%
GetTimeline_SQLite/events-1000-4          1.639Mi ±  0%
geomean                                   67.13Ki

                                   │ baseline-bench.txt │
                                   │     allocs/op      │
EventStoreAppend_InMemory-4                  7.000 ± 0%
EventStoreAppend_SQLite-4                    53.00 ± 0%
GetTimeline_InMemory/events-10-4             125.0 ± 0%
GetTimeline_InMemory/events-50-4             653.0 ± 0%
GetTimeline_InMemory/events-100-4           1.306k ± 0%
GetTimeline_InMemory/events-500-4           6.514k ± 0%
GetTimeline_InMemory/events-1000-4          13.02k ± 0%
GetTimeline_SQLite/events-10-4               382.0 ± 0%
GetTimeline_SQLite/events-50-4              1.852k ± 0%
GetTimeline_SQLite/events-100-4             3.681k ± 0%
GetTimeline_SQLite/events-500-4             18.54k ± 0%
GetTimeline_SQLite/events-1000-4            37.29k ± 0%
geomean                                     1.162k

cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                                   │ benchmark-results.txt │
                                   │        sec/op         │
EventStoreAppend_InMemory-4                   1.098µ ± 28%
EventStoreAppend_SQLite-4                     944.7µ ±  3%
GetTimeline_InMemory/events-10-4              13.89µ ± 10%
GetTimeline_InMemory/events-50-4              77.71µ ±  4%
GetTimeline_InMemory/events-100-4             154.7µ ± 21%
GetTimeline_InMemory/events-500-4             636.9µ ±  1%
GetTimeline_InMemory/events-1000-4            1.300m ±  1%
GetTimeline_SQLite/events-10-4                85.17µ ±  1%
GetTimeline_SQLite/events-50-4                242.8µ ±  1%
GetTimeline_SQLite/events-100-4               437.1µ ±  1%
GetTimeline_SQLite/events-500-4               1.960m ±  2%
GetTimeline_SQLite/events-1000-4              3.835m ±  2%
geomean                                       215.0µ

                                   │ benchmark-results.txt │
                                   │         B/op          │
EventStoreAppend_InMemory-4                    774.5 ± 10%
EventStoreAppend_SQLite-4                    1.986Ki ±  1%
GetTimeline_InMemory/events-10-4             7.953Ki ±  0%
GetTimeline_InMemory/events-50-4             46.62Ki ±  0%
GetTimeline_InMemory/events-100-4            94.48Ki ±  0%
GetTimeline_InMemory/events-500-4            472.8Ki ±  0%
GetTimeline_InMemory/events-1000-4           944.3Ki ±  0%
GetTimeline_SQLite/events-10-4               16.74Ki ±  0%
GetTimeline_SQLite/events-50-4               87.14Ki ±  0%
GetTimeline_SQLite/events-100-4              175.4Ki ±  0%
GetTimeline_SQLite/events-500-4              846.1Ki ±  0%
GetTimeline_SQLite/events-1000-4             1.639Mi ±  0%
geomean                                      67.24Ki

                                   │ benchmark-results.txt │
                                   │       allocs/op       │
EventStoreAppend_InMemory-4                     7.000 ± 0%
EventStoreAppend_SQLite-4                       53.00 ± 0%
GetTimeline_InMemory/events-10-4                125.0 ± 0%
GetTimeline_InMemory/events-50-4                653.0 ± 0%
GetTimeline_InMemory/events-100-4              1.306k ± 0%
GetTimeline_InMemory/events-500-4              6.514k ± 0%
GetTimeline_InMemory/events-1000-4             13.02k ± 0%
GetTimeline_SQLite/events-10-4                  382.0 ± 0%
GetTimeline_SQLite/events-50-4                 1.852k ± 0%
GetTimeline_SQLite/events-100-4                3.681k ± 0%
GetTimeline_SQLite/events-500-4                18.54k ± 0%
GetTimeline_SQLite/events-1000-4               37.29k ± 0%
geomean                                        1.162k

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

@intel352 intel352 merged commit 006b368 into main Mar 12, 2026
11 checks passed
@intel352 intel352 deleted the copilot/fix-double-encoding-jsonb-types branch March 12, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

step.json_response: double-encoding of PostgreSQL json/jsonb types via pgx driver

3 participants