github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/changefeedccl/encoder_test.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  package changefeedccl
    10  
    11  import (
    12  	"context"
    13  	gosql "database/sql"
    14  	"encoding/binary"
    15  	gojson "encoding/json"
    16  	"fmt"
    17  	"net/http"
    18  	"net/http/httptest"
    19  	"testing"
    20  
    21  	"github.com/cockroachdb/cockroach-go/crdb"
    22  	"github.com/cockroachdb/cockroach/pkg/ccl/changefeedccl/cdctest"
    23  	"github.com/cockroachdb/cockroach/pkg/ccl/changefeedccl/changefeedbase"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    26  	"github.com/cockroachdb/cockroach/pkg/testutils"
    27  	"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
    28  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    29  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    30  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    31  	"github.com/cockroachdb/cockroach/pkg/workload/ledger"
    32  	"github.com/cockroachdb/cockroach/pkg/workload/workloadsql"
    33  	"github.com/cockroachdb/errors"
    34  	"github.com/linkedin/goavro"
    35  	"github.com/stretchr/testify/require"
    36  )
    37  
    38  func TestEncoders(t *testing.T) {
    39  	defer leaktest.AfterTest(t)()
    40  
    41  	tableDesc, err := parseTableDesc(`CREATE TABLE foo (a INT PRIMARY KEY, b STRING)`)
    42  	require.NoError(t, err)
    43  	row := sqlbase.EncDatumRow{
    44  		sqlbase.EncDatum{Datum: tree.NewDInt(1)},
    45  		sqlbase.EncDatum{Datum: tree.NewDString(`bar`)},
    46  	}
    47  	ts := hlc.Timestamp{WallTime: 1, Logical: 2}
    48  
    49  	var opts []map[string]string
    50  	for _, f := range []string{string(changefeedbase.OptFormatJSON), string(changefeedbase.OptFormatAvro)} {
    51  		for _, e := range []string{
    52  			string(changefeedbase.OptEnvelopeKeyOnly), string(changefeedbase.OptEnvelopeRow), string(changefeedbase.OptEnvelopeWrapped),
    53  		} {
    54  			opts = append(opts,
    55  				map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e},
    56  				map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptDiff: ``},
    57  				map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptUpdatedTimestamps: ``},
    58  				map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptUpdatedTimestamps: ``, changefeedbase.OptDiff: ``},
    59  			)
    60  		}
    61  	}
    62  
    63  	expecteds := map[string]struct {
    64  		// Either err is set or all of insert, delete, and resolved are.
    65  		err      string
    66  		insert   string
    67  		delete   string
    68  		resolved string
    69  	}{
    70  		`format=json,envelope=key_only`: {
    71  			insert:   `[1]->`,
    72  			delete:   `[1]->`,
    73  			resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`,
    74  		},
    75  		`format=json,envelope=key_only,updated`: {
    76  			insert:   `[1]->`,
    77  			delete:   `[1]->`,
    78  			resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`,
    79  		},
    80  		`format=json,envelope=key_only,diff`: {
    81  			err: `diff is only usable with envelope=wrapped`,
    82  		},
    83  		`format=json,envelope=key_only,updated,diff`: {
    84  			err: `diff is only usable with envelope=wrapped`,
    85  		},
    86  		`format=json,envelope=row`: {
    87  			insert:   `[1]->{"a": 1, "b": "bar"}`,
    88  			delete:   `[1]->`,
    89  			resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`,
    90  		},
    91  		`format=json,envelope=row,updated`: {
    92  			insert:   `[1]->{"__crdb__": {"updated": "1.0000000002"}, "a": 1, "b": "bar"}`,
    93  			delete:   `[1]->`,
    94  			resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`,
    95  		},
    96  		`format=json,envelope=row,diff`: {
    97  			err: `diff is only usable with envelope=wrapped`,
    98  		},
    99  		`format=json,envelope=row,updated,diff`: {
   100  			err: `diff is only usable with envelope=wrapped`,
   101  		},
   102  		`format=json,envelope=wrapped`: {
   103  			insert:   `[1]->{"after": {"a": 1, "b": "bar"}}`,
   104  			delete:   `[1]->{"after": null}`,
   105  			resolved: `{"resolved":"1.0000000002"}`,
   106  		},
   107  		`format=json,envelope=wrapped,updated`: {
   108  			insert:   `[1]->{"after": {"a": 1, "b": "bar"}, "updated": "1.0000000002"}`,
   109  			delete:   `[1]->{"after": null, "updated": "1.0000000002"}`,
   110  			resolved: `{"resolved":"1.0000000002"}`,
   111  		},
   112  		`format=json,envelope=wrapped,diff`: {
   113  			insert:   `[1]->{"after": {"a": 1, "b": "bar"}, "before": null}`,
   114  			delete:   `[1]->{"after": null, "before": {"a": 1, "b": "bar"}}`,
   115  			resolved: `{"resolved":"1.0000000002"}`,
   116  		},
   117  		`format=json,envelope=wrapped,updated,diff`: {
   118  			insert:   `[1]->{"after": {"a": 1, "b": "bar"}, "before": null, "updated": "1.0000000002"}`,
   119  			delete:   `[1]->{"after": null, "before": {"a": 1, "b": "bar"}, "updated": "1.0000000002"}`,
   120  			resolved: `{"resolved":"1.0000000002"}`,
   121  		},
   122  		`format=experimental_avro,envelope=key_only`: {
   123  			insert:   `{"a":{"long":1}}->`,
   124  			delete:   `{"a":{"long":1}}->`,
   125  			resolved: `{"resolved":{"string":"1.0000000002"}}`,
   126  		},
   127  		`format=experimental_avro,envelope=key_only,updated`: {
   128  			err: `updated is only usable with envelope=wrapped`,
   129  		},
   130  		`format=experimental_avro,envelope=key_only,diff`: {
   131  			err: `diff is only usable with envelope=wrapped`,
   132  		},
   133  		`format=experimental_avro,envelope=key_only,updated,diff`: {
   134  			err: `updated is only usable with envelope=wrapped`,
   135  		},
   136  		`format=experimental_avro,envelope=row`: {
   137  			err: `envelope=row is not supported with format=experimental_avro`,
   138  		},
   139  		`format=experimental_avro,envelope=row,updated`: {
   140  			err: `envelope=row is not supported with format=experimental_avro`,
   141  		},
   142  		`format=experimental_avro,envelope=row,diff`: {
   143  			err: `envelope=row is not supported with format=experimental_avro`,
   144  		},
   145  		`format=experimental_avro,envelope=row,updated,diff`: {
   146  			err: `envelope=row is not supported with format=experimental_avro`,
   147  		},
   148  		`format=experimental_avro,envelope=wrapped`: {
   149  			insert: `{"a":{"long":1}}->` +
   150  				`{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}}}`,
   151  			delete:   `{"a":{"long":1}}->{"after":null}`,
   152  			resolved: `{"resolved":{"string":"1.0000000002"}}`,
   153  		},
   154  		`format=experimental_avro,envelope=wrapped,updated`: {
   155  			insert: `{"a":{"long":1}}->` +
   156  				`{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` +
   157  				`"updated":{"string":"1.0000000002"}}`,
   158  			delete:   `{"a":{"long":1}}->{"after":null,"updated":{"string":"1.0000000002"}}`,
   159  			resolved: `{"resolved":{"string":"1.0000000002"}}`,
   160  		},
   161  		`format=experimental_avro,envelope=wrapped,diff`: {
   162  			insert: `{"a":{"long":1}}->` +
   163  				`{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` +
   164  				`"before":null}`,
   165  			delete: `{"a":{"long":1}}->` +
   166  				`{"after":null,` +
   167  				`"before":{"foo_before":{"a":{"long":1},"b":{"string":"bar"}}}}`,
   168  			resolved: `{"resolved":{"string":"1.0000000002"}}`,
   169  		},
   170  		`format=experimental_avro,envelope=wrapped,updated,diff`: {
   171  			insert: `{"a":{"long":1}}->` +
   172  				`{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` +
   173  				`"before":null,` +
   174  				`"updated":{"string":"1.0000000002"}}`,
   175  			delete: `{"a":{"long":1}}->` +
   176  				`{"after":null,` +
   177  				`"before":{"foo_before":{"a":{"long":1},"b":{"string":"bar"}}},` +
   178  				`"updated":{"string":"1.0000000002"}}`,
   179  			resolved: `{"resolved":{"string":"1.0000000002"}}`,
   180  		},
   181  	}
   182  
   183  	for _, o := range opts {
   184  		name := fmt.Sprintf("format=%s,envelope=%s", o[changefeedbase.OptFormat], o[changefeedbase.OptEnvelope])
   185  		if _, ok := o[changefeedbase.OptUpdatedTimestamps]; ok {
   186  			name += `,updated`
   187  		}
   188  		if _, ok := o[changefeedbase.OptDiff]; ok {
   189  			name += `,diff`
   190  		}
   191  		t.Run(name, func(t *testing.T) {
   192  			expected := expecteds[name]
   193  
   194  			var rowStringFn func([]byte, []byte) string
   195  			var resolvedStringFn func([]byte) string
   196  			switch o[changefeedbase.OptFormat] {
   197  			case string(changefeedbase.OptFormatJSON):
   198  				rowStringFn = func(k, v []byte) string { return fmt.Sprintf(`%s->%s`, k, v) }
   199  				resolvedStringFn = func(r []byte) string { return string(r) }
   200  			case string(changefeedbase.OptFormatAvro):
   201  				reg := makeTestSchemaRegistry()
   202  				defer reg.Close()
   203  				o[changefeedbase.OptConfluentSchemaRegistry] = reg.server.URL
   204  				rowStringFn = func(k, v []byte) string {
   205  					key, value := avroToJSON(t, reg, k), avroToJSON(t, reg, v)
   206  					return fmt.Sprintf(`%s->%s`, key, value)
   207  				}
   208  				resolvedStringFn = func(r []byte) string {
   209  					return string(avroToJSON(t, reg, r))
   210  				}
   211  			default:
   212  				t.Fatalf(`unknown format: %s`, o[changefeedbase.OptFormat])
   213  			}
   214  
   215  			e, err := getEncoder(o)
   216  			if len(expected.err) > 0 {
   217  				require.EqualError(t, err, expected.err)
   218  				return
   219  			}
   220  			require.NoError(t, err)
   221  
   222  			rowInsert := encodeRow{
   223  				datums:        row,
   224  				updated:       ts,
   225  				tableDesc:     tableDesc,
   226  				prevDatums:    nil,
   227  				prevTableDesc: tableDesc,
   228  			}
   229  			keyInsert, err := e.EncodeKey(context.Background(), rowInsert)
   230  			require.NoError(t, err)
   231  			keyInsert = append([]byte(nil), keyInsert...)
   232  			valueInsert, err := e.EncodeValue(context.Background(), rowInsert)
   233  			require.NoError(t, err)
   234  			require.Equal(t, expected.insert, rowStringFn(keyInsert, valueInsert))
   235  
   236  			rowDelete := encodeRow{
   237  				datums:        row,
   238  				deleted:       true,
   239  				prevDatums:    row,
   240  				updated:       ts,
   241  				tableDesc:     tableDesc,
   242  				prevTableDesc: tableDesc,
   243  			}
   244  			keyDelete, err := e.EncodeKey(context.Background(), rowDelete)
   245  			require.NoError(t, err)
   246  			keyDelete = append([]byte(nil), keyDelete...)
   247  			valueDelete, err := e.EncodeValue(context.Background(), rowDelete)
   248  			require.NoError(t, err)
   249  			require.Equal(t, expected.delete, rowStringFn(keyDelete, valueDelete))
   250  
   251  			resolved, err := e.EncodeResolvedTimestamp(context.Background(), tableDesc.Name, ts)
   252  			require.NoError(t, err)
   253  			require.Equal(t, expected.resolved, resolvedStringFn(resolved))
   254  		})
   255  	}
   256  }
   257  
   258  type testSchemaRegistry struct {
   259  	server *httptest.Server
   260  	mu     struct {
   261  		syncutil.Mutex
   262  		idAlloc int32
   263  		schemas map[int32]string
   264  	}
   265  }
   266  
   267  func makeTestSchemaRegistry() *testSchemaRegistry {
   268  	r := &testSchemaRegistry{}
   269  	r.mu.schemas = make(map[int32]string)
   270  	r.server = httptest.NewServer(http.HandlerFunc(r.Register))
   271  	return r
   272  }
   273  
   274  func (r *testSchemaRegistry) Close() {
   275  	r.server.Close()
   276  }
   277  
   278  func (r *testSchemaRegistry) Register(hw http.ResponseWriter, hr *http.Request) {
   279  	type confluentSchemaVersionRequest struct {
   280  		Schema string `json:"schema"`
   281  	}
   282  	type confluentSchemaVersionResponse struct {
   283  		ID int32 `json:"id"`
   284  	}
   285  	if err := func() error {
   286  		defer hr.Body.Close()
   287  		var req confluentSchemaVersionRequest
   288  		if err := gojson.NewDecoder(hr.Body).Decode(&req); err != nil {
   289  			return err
   290  		}
   291  
   292  		r.mu.Lock()
   293  		id := r.mu.idAlloc
   294  		r.mu.idAlloc++
   295  		r.mu.schemas[id] = req.Schema
   296  		r.mu.Unlock()
   297  
   298  		res, err := gojson.Marshal(confluentSchemaVersionResponse{ID: id})
   299  		if err != nil {
   300  			return err
   301  		}
   302  
   303  		hw.Header().Set(`Content-type`, `application/json`)
   304  		_, _ = hw.Write(res)
   305  		return nil
   306  	}(); err != nil {
   307  		http.Error(hw, err.Error(), http.StatusInternalServerError)
   308  	}
   309  }
   310  
   311  func (r *testSchemaRegistry) encodedAvroToNative(b []byte) (interface{}, error) {
   312  	if len(b) == 0 || b[0] != confluentAvroWireFormatMagic {
   313  		return ``, errors.Errorf(`bad magic byte`)
   314  	}
   315  	b = b[1:]
   316  	if len(b) < 4 {
   317  		return ``, errors.Errorf(`missing registry id`)
   318  	}
   319  	id := int32(binary.BigEndian.Uint32(b[:4]))
   320  	b = b[4:]
   321  
   322  	r.mu.Lock()
   323  	jsonSchema := r.mu.schemas[id]
   324  	r.mu.Unlock()
   325  	codec, err := goavro.NewCodec(jsonSchema)
   326  	if err != nil {
   327  		return ``, err
   328  	}
   329  	native, _, err := codec.NativeFromBinary(b)
   330  	return native, err
   331  }
   332  
   333  func TestAvroEncoder(t *testing.T) {
   334  	defer leaktest.AfterTest(t)()
   335  
   336  	testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) {
   337  		ctx := context.Background()
   338  		reg := makeTestSchemaRegistry()
   339  		defer reg.Close()
   340  
   341  		sqlDB := sqlutils.MakeSQLRunner(db)
   342  		sqlDB.Exec(t, `CREATE TABLE foo (a INT PRIMARY KEY, b STRING)`)
   343  		var ts1 string
   344  		sqlDB.QueryRow(t,
   345  			`INSERT INTO foo VALUES (1, 'bar'), (2, NULL) RETURNING cluster_logical_timestamp()`,
   346  		).Scan(&ts1)
   347  
   348  		foo := feed(t, f, `CREATE CHANGEFEED FOR foo `+
   349  			`WITH format=$1, confluent_schema_registry=$2, diff, resolved`,
   350  			changefeedbase.OptFormatAvro, reg.server.URL)
   351  		defer closeFeed(t, foo)
   352  		assertPayloadsAvro(t, reg, foo, []string{
   353  			`foo: {"a":{"long":1}}->{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},"before":null}`,
   354  			`foo: {"a":{"long":2}}->{"after":{"foo":{"a":{"long":2},"b":null}},"before":null}`,
   355  		})
   356  		resolved := expectResolvedTimestampAvro(t, reg, foo)
   357  		if ts := parseTimeToHLC(t, ts1); resolved.LessEq(ts) {
   358  			t.Fatalf(`expected a resolved timestamp greater than %s got %s`, ts, resolved)
   359  		}
   360  
   361  		fooUpdated := feed(t, f, `CREATE CHANGEFEED FOR foo `+
   362  			`WITH format=$1, confluent_schema_registry=$2, diff, updated`,
   363  			changefeedbase.OptFormatAvro, reg.server.URL)
   364  		defer closeFeed(t, fooUpdated)
   365  		// Skip over the first two rows since we don't know the statement timestamp.
   366  		_, err := fooUpdated.Next()
   367  		require.NoError(t, err)
   368  		_, err = fooUpdated.Next()
   369  		require.NoError(t, err)
   370  
   371  		var ts2 string
   372  		require.NoError(t, crdb.ExecuteTx(ctx, db, nil /* txopts */, func(tx *gosql.Tx) error {
   373  			return tx.QueryRow(
   374  				`INSERT INTO foo VALUES (3, 'baz') RETURNING cluster_logical_timestamp()`,
   375  			).Scan(&ts2)
   376  		}))
   377  		assertPayloadsAvro(t, reg, fooUpdated, []string{
   378  			`foo: {"a":{"long":3}}->{"after":{"foo":{"a":{"long":3},"b":{"string":"baz"}}},` +
   379  				`"before":null,` +
   380  				`"updated":{"string":"` + ts2 + `"}}`,
   381  		})
   382  	}
   383  
   384  	t.Run(`sinkless`, sinklessTest(testFn))
   385  	t.Run(`enterprise`, enterpriseTest(testFn))
   386  }
   387  
   388  func TestAvroMigrateToUnsupportedColumn(t *testing.T) {
   389  	defer leaktest.AfterTest(t)()
   390  
   391  	testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) {
   392  		reg := makeTestSchemaRegistry()
   393  		defer reg.Close()
   394  
   395  		sqlDB := sqlutils.MakeSQLRunner(db)
   396  		sqlDB.Exec(t, `CREATE TABLE foo (a INT PRIMARY KEY)`)
   397  		sqlDB.Exec(t, `INSERT INTO foo VALUES (1)`)
   398  
   399  		foo := feed(t, f, `CREATE CHANGEFEED FOR foo `+
   400  			`WITH format=$1, confluent_schema_registry=$2`,
   401  			changefeedbase.OptFormatAvro, reg.server.URL)
   402  		defer closeFeed(t, foo)
   403  		assertPayloadsAvro(t, reg, foo, []string{
   404  			`foo: {"a":{"long":1}}->{"after":{"foo":{"a":{"long":1}}}}`,
   405  		})
   406  
   407  		sqlDB.Exec(t, `ALTER TABLE foo ADD COLUMN b OID`)
   408  		sqlDB.Exec(t, `INSERT INTO foo VALUES (2, 3::OID)`)
   409  		if _, err := foo.Next(); !testutils.IsError(err, `type OID not yet supported with avro`) {
   410  			t.Fatalf(`expected "type OID not yet supported with avro" error got: %+v`, err)
   411  		}
   412  	}
   413  
   414  	t.Run(`sinkless`, sinklessTest(testFn))
   415  	t.Run(`enterprise`, enterpriseTest(testFn))
   416  }
   417  
   418  func TestAvroLedger(t *testing.T) {
   419  	defer leaktest.AfterTest(t)()
   420  
   421  	testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) {
   422  		reg := makeTestSchemaRegistry()
   423  		defer reg.Close()
   424  
   425  		ctx := context.Background()
   426  		gen := ledger.FromFlags(`--customers=1`)
   427  		var l workloadsql.InsertsDataLoader
   428  		_, err := workloadsql.Setup(ctx, db, gen, l)
   429  		require.NoError(t, err)
   430  
   431  		ledger := feed(t, f, `CREATE CHANGEFEED FOR customer, transaction, entry, session
   432  	                       WITH format=$1, confluent_schema_registry=$2
   433  	               `, changefeedbase.OptFormatAvro, reg.server.URL)
   434  		defer closeFeed(t, ledger)
   435  
   436  		assertPayloadsAvro(t, reg, ledger, []string{
   437  			`customer: {"id":{"long":0}}->{"after":{"customer":{"balance":{"bytes.decimal":"0"},"created":{"long.timestamp-micros":"2114-03-27T13:14:27.287114Z"},"credit_limit":null,"currency_code":{"string":"XVL"},"id":{"long":0},"identifier":{"string":"0"},"is_active":{"boolean":true},"is_system_customer":{"boolean":true},"name":null,"sequence_number":{"long":-1}}}}`,
   438  			`entry: {"id":{"long":1543039099823358511}}->{"after":{"entry":{"amount":{"bytes.decimal":"0"},"created_ts":{"long.timestamp-micros":"1990-12-09T23:47:23.811124Z"},"customer_id":{"long":0},"id":{"long":1543039099823358511},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`,
   439  			`entry: {"id":{"long":2244708090865615074}}->{"after":{"entry":{"amount":{"bytes.decimal":"1/50"},"created_ts":{"long.timestamp-micros":"2075-11-08T22:07:12.055686Z"},"customer_id":{"long":0},"id":{"long":2244708090865615074},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`,
   440  			`entry: {"id":{"long":3305628230121721621}}->{"after":{"entry":{"amount":{"bytes.decimal":"1/25"},"created_ts":{"long.timestamp-micros":"2185-01-30T21:38:15.06669Z"},"customer_id":{"long":0},"id":{"long":3305628230121721621},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`,
   441  			`entry: {"id":{"long":4151935814835861840}}->{"after":{"entry":{"amount":{"bytes.decimal":"3/50"},"created_ts":{"long.timestamp-micros":"1684-10-05T17:51:40.795101Z"},"customer_id":{"long":0},"id":{"long":4151935814835861840},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`,
   442  			`entry: {"id":{"long":5577006791947779410}}->{"after":{"entry":{"amount":{"bytes.decimal":"0"},"created_ts":{"long.timestamp-micros":"2185-11-07T09:42:42.666146Z"},"customer_id":{"long":0},"id":{"long":5577006791947779410},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`,
   443  			`entry: {"id":{"long":6640668014774057861}}->{"after":{"entry":{"amount":{"bytes.decimal":"-1/50"},"created_ts":{"long.timestamp-micros":"1690-05-19T13:29:46.145044Z"},"customer_id":{"long":0},"id":{"long":6640668014774057861},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`,
   444  			`entry: {"id":{"long":7414159922357799360}}->{"after":{"entry":{"amount":{"bytes.decimal":"-1/25"},"created_ts":{"long.timestamp-micros":"1706-02-05T02:38:08.15195Z"},"customer_id":{"long":0},"id":{"long":7414159922357799360},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`,
   445  			`entry: {"id":{"long":8475284246537043955}}->{"after":{"entry":{"amount":{"bytes.decimal":"-3/50"},"created_ts":{"long.timestamp-micros":"2048-07-21T10:02:40.114474Z"},"customer_id":{"long":0},"id":{"long":8475284246537043955},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`,
   446  			`session: {"session_id":{"string":"pLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6AWX40IV"}}->{"after":{"session":{"data":{"string":"SP3NcHciWvqZTa3N06RxRTZHWUsaD7HEdz1ThbXfQ7pYSQ4n378l2VQKGNbSuJE0fQbzONJAAwdCxmM9BIabKERsUhPNmMmdf3eSJyYtqwcFiUILzXv3fcNIrWO8sToFgoilA1U2WxNeW2gdgUVDsEWJ88aX8tLF"},"expiry_timestamp":{"long.timestamp-micros":"2052-05-14T04:02:49.264975Z"},"last_update":{"long.timestamp-micros":"2070-03-19T02:10:22.552438Z"},"session_id":{"string":"pLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6AWX40IV"}}}}`,
   447  			`transaction: {"external_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}->{"after":{"transaction":{"context":{"string":"BpLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6"},"created_ts":{"long.timestamp-micros":"2178-08-01T19:10:30.064819Z"},"external_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"},"response":{"bytes":"MDZSeFJUWkhXVXNhRDdIRWR6MVRoYlhmUTdwWVNRNG4zNzhsMlZRS0dOYlN1SkUwZlFiek9OSkFBd2RDeG1NOUJJYWJLRVJzVWhQTm1NbWRmM2VTSnlZdHF3Y0ZpVUlMelh2M2ZjTklyV084c1RvRmdvaWxBMVUyV3hOZVcyZ2RnVVZEc0VXSjg4YVg4dExGSjk1cVlVN1VyTjljdGVjd1p0NlM1empoRDF0WFJUbWtZS1FvTjAyRm1XblFTSzN3UkM2VUhLM0txQXR4alAzWm1EMmp0dDR6Z3I2TWVVam9BamNPMGF6TW10VTRZdHYxUDhPUG1tU05hOThkN3RzdGF4eTZuYWNuSkJTdUZwT2h5SVhFN1BKMURoVWtMWHFZWW5FTnVucWRzd3BUdzVVREdEUzM0bVNQWUs4dm11YjNYOXVYSXU3Rk5jSmpBUlFUM1JWaFZydDI0UDdpNnhDckw2RmM0R2N1SEMxNGthdW5BVFVQUkhqR211Vm14SHN5enpCYnlPb25xVlVTREsxVg=="},"reversed_by":null,"systimestamp":{"long.timestamp-micros":"2215-07-28T23:47:01.795499Z"},"tcomment":null,"transaction_type_reference":{"long":400},"username":{"string":"WX40IVUWSP3NcHciWvqZ"}}}}`,
   448  			`transaction: {"external_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}->{"after":{"transaction":{"context":{"string":"KSiOW5eQ8sklpgstrQZtAcrsGvPnYSXMOpFIpPzS8iI5N2gN7lD1rYjT"},"created_ts":{"long.timestamp-micros":"2062-07-27T13:21:35.213969Z"},"external_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"},"response":{"bytes":"bWdkbHVWOFVvcWpRM1JBTTRTWjNzT0M4ZnlzZXN5NnRYeVZ5WTBnWkE1aVNJUjM4MFVPVWFwQTlPRmpuRWtiaHF6MlRZSlZIWUFtTHI5R0kyMlo3NFVmNjhDMFRRb2RDdWF0NmhmWmZSYmFlV1pJSFExMGJsSjVqQUd2VVRpWWJOWHZPcWowYlRUM24xNmNqQVNEN29qN2RPbVlVbTFua3AybnVvWTZGZlgzcVFHY09SbHZ2UHdHaHNDZWlZTmpvTVRoUXBFc0ZrSVpZVUxxNFFORzc1M25mamJYdENaUm4xSmVZV1hpUW1IWjJZMWIxb1lZbUtBS05aQjF1MGt1TU5ZbEFISW5hY1JoTkFzakd6bnBKSXZZdmZqWXk3MXV4OVI5SkRNQUMxRUtOSGFZVWNlekk4OHRHYmdwbWFGaXdIV09sUFQ5RUJVcHh6MHlCSnZGM1BKcW5jejVwMnpnVVhDcm9kZTV6UG5pNjJQV1dtMk5pSWVkSUxFaExLVVNHVWRNU1R5N1pmcjRyY2RJTw=="},"reversed_by":null,"systimestamp":{"long.timestamp-micros":"2229-01-11T00:56:37.706179Z"},"tcomment":null,"transaction_type_reference":{"long":400},"username":{"string":"XJXORIpfMGxOaIIFFFts"}}}}`,
   449  		})
   450  	}
   451  
   452  	t.Run(`sinkless`, sinklessTest(testFn))
   453  	t.Run(`enterprise`, enterpriseTest(testFn))
   454  }