From 97db2418f211eea3c600ec08fe13a18fca59b133 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Fri, 3 Jul 2026 15:55:52 +0200 Subject: [PATCH] Fix indexing and queries for numeric values with the high bit set Numeric attribute values of 2^63 or more could not be bound to SQLite (Go's database/sql rejects uint64 with the high bit set), so a single entity with such an annotation permanently wedged the event follower on every node. Store values with the top bit flipped (value XOR 2^63): the full uint64 range maps one-to-one onto SQLite's signed INTEGER with order preserved, so equality, IN and range comparisons all stay correct. Existing rows are re-encoded by migration 000002; wedged nodes heal on upgrade and resume from last_block. Also fix %q being applied to uint64 in bitmap cache error messages, which rendered values as unicode garbage in logs. Co-Authored-By: Claude Fable 5 --- bitmap_cache.go | 16 +- query/evaluate.go | 16 +- sqlitestore_test.go | 218 ++++++++++++++++++ store/evals.sql.go | 16 +- store/models.go | 2 +- store/numeric_value.go | 50 ++++ store/queries.sql.go | 6 +- .../000002_rebias_numeric_values.up.sql | 10 + store/sqlc.yaml | 5 +- 9 files changed, 310 insertions(+), 29 deletions(-) create mode 100644 store/numeric_value.go create mode 100644 store/schema/000002_rebias_numeric_values.up.sql diff --git a/bitmap_cache.go b/bitmap_cache.go index 13ed061..0aacf35 100644 --- a/bitmap_cache.go +++ b/bitmap_cache.go @@ -80,10 +80,10 @@ func (c *bitmapCache) AddToNumericBitmap(ctx context.Context, name string, value k := nameValue[uint64]{name: name, value: value} bitmap, ok := c.numericBitmaps[k] if !ok { - bitmap, err = c.st.GetNumericAttributeValueBitmap(ctx, store.GetNumericAttributeValueBitmapParams{Name: name, Value: value}) + bitmap, err = c.st.GetNumericAttributeValueBitmap(ctx, store.GetNumericAttributeValueBitmapParams{Name: name, Value: store.NumericValue(value)}) if err != nil && err != sql.ErrNoRows { - return fmt.Errorf("failed to get numeric attribute %q value %q bitmap: %w", name, value, err) + return fmt.Errorf("failed to get numeric attribute %q value %d bitmap: %w", name, value, err) } if bitmap == nil { @@ -103,10 +103,10 @@ func (c *bitmapCache) RemoveFromNumericBitmap(ctx context.Context, name string, k := nameValue[uint64]{name: name, value: value} bitmap, ok := c.numericBitmaps[k] if !ok { - bitmap, err = c.st.GetNumericAttributeValueBitmap(ctx, store.GetNumericAttributeValueBitmapParams{Name: name, Value: value}) + bitmap, err = c.st.GetNumericAttributeValueBitmap(ctx, store.GetNumericAttributeValueBitmapParams{Name: name, Value: store.NumericValue(value)}) if err != nil && err != sql.ErrNoRows { - return fmt.Errorf("failed to get numeric attribute %q value %q bitmap: %w", name, value, err) + return fmt.Errorf("failed to get numeric attribute %q value %d bitmap: %w", name, value, err) } if bitmap == nil { @@ -172,16 +172,16 @@ func (c *bitmapCache) Flush(ctx context.Context) (err error) { for k, bitmap := range c.numericBitmaps { if bitmap.IsEmpty() { - err = c.st.DeleteNumericAttributeValueBitmap(ctx, store.DeleteNumericAttributeValueBitmapParams{Name: k.name, Value: k.value}) + err = c.st.DeleteNumericAttributeValueBitmap(ctx, store.DeleteNumericAttributeValueBitmapParams{Name: k.name, Value: store.NumericValue(k.value)}) if err != nil { - return fmt.Errorf("failed to delete numeric attribute %q value %q bitmap: %w", k.name, k.value, err) + return fmt.Errorf("failed to delete numeric attribute %q value %d bitmap: %w", k.name, k.value, err) } continue } - err = c.st.UpsertNumericAttributeValueBitmap(ctx, store.UpsertNumericAttributeValueBitmapParams{Name: k.name, Value: k.value, Bitmap: bitmap}) + err = c.st.UpsertNumericAttributeValueBitmap(ctx, store.UpsertNumericAttributeValueBitmapParams{Name: k.name, Value: store.NumericValue(k.value), Bitmap: bitmap}) if err != nil { - return fmt.Errorf("failed to upsert numeric attribute %q value %q bitmap: %w", k.name, k.value, err) + return fmt.Errorf("failed to upsert numeric attribute %q value %d bitmap: %w", k.name, k.value, err) } } return nil diff --git a/query/evaluate.go b/query/evaluate.go index c9cb3df..018d33b 100644 --- a/query/evaluate.go +++ b/query/evaluate.go @@ -150,7 +150,7 @@ func (e *LessThan) Evaluate( } else { bitmaps, err = q.EvaluateNumericAttributeValueLowerThan(ctx, store.EvaluateNumericAttributeValueLowerThanParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err != nil { return nil, err @@ -184,7 +184,7 @@ func (e *LessOrEqualThan) Evaluate( } else { bitmaps, err = q.EvaluateNumericAttributeValueLessOrEqualThan(ctx, store.EvaluateNumericAttributeValueLessOrEqualThanParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err != nil { return nil, err @@ -219,7 +219,7 @@ func (e *GreaterThan) Evaluate( } else { bitmaps, err = q.EvaluateNumericAttributeValueGreaterThan(ctx, store.EvaluateNumericAttributeValueGreaterThanParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err != nil { return nil, err @@ -254,7 +254,7 @@ func (e *GreaterOrEqualThan) Evaluate( } else { bitmaps, err = q.EvaluateNumericAttributeValueGreaterOrEqualThan(ctx, store.EvaluateNumericAttributeValueGreaterOrEqualThanParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err != nil { return nil, err @@ -318,7 +318,7 @@ func (e *Equality) Evaluate( var bitmaps []*store.Bitmap bitmaps, err = q.EvaluateNumericAttributeValueNotEqual(ctx, store.EvaluateNumericAttributeValueNotEqualParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err != nil { return nil, err @@ -333,7 +333,7 @@ func (e *Equality) Evaluate( } else { bitmap, err := q.EvaluateNumericAttributeValueEqual(ctx, store.EvaluateNumericAttributeValueEqualParams{ Name: e.Var, - Value: *e.Value.Number, + Value: store.NumericValue(*e.Value.Number), }) if err == sql.ErrNoRows { @@ -389,7 +389,7 @@ func (e *Inclusion) Evaluate( if e.IsNot { bitmaps, err = q.EvaluateNumericAttributeValueNotInclusion(ctx, store.EvaluateNumericAttributeValueNotInclusionParams{ Name: e.Var, - Values: e.Values.Numbers, + Values: store.NumericValues(e.Values.Numbers), }) if err != nil { return nil, err @@ -397,7 +397,7 @@ func (e *Inclusion) Evaluate( } else { bitmaps, err = q.EvaluateNumericAttributeValueInclusion(ctx, store.EvaluateNumericAttributeValueInclusionParams{ Name: e.Var, - Values: e.Values.Numbers, + Values: store.NumericValues(e.Values.Numbers), }) if err != nil { return nil, err diff --git a/sqlitestore_test.go b/sqlitestore_test.go index bf0c8f0..efe6320 100644 --- a/sqlitestore_test.go +++ b/sqlitestore_test.go @@ -2,13 +2,18 @@ package sqlitebitmapstore_test import ( "context" + "database/sql" "errors" "log/slog" + "math" "os" "path/filepath" "strings" "github.com/ethereum/go-ethereum/common" + "github.com/golang-migrate/migrate/v4" + migratesqlite3 "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,6 +21,7 @@ import ( "github.com/Arkiv-Network/arkiv-events/events" sqlitebitmapstore "github.com/Arkiv-Network/sqlite-bitmap-store" "github.com/Arkiv-Network/sqlite-bitmap-store/pusher" + "github.com/Arkiv-Network/sqlite-bitmap-store/query" "github.com/Arkiv-Network/sqlite-bitmap-store/store" ) @@ -1101,4 +1107,216 @@ var _ = Describe("SQLiteStore", func() { Expect(err).NotTo(HaveOccurred()) }) }) + + Describe("FollowEvents with numeric attribute values of 2^63 or more", func() { + // Regression test: values with the top bit set used to fail at the + // database layer ("uint64 values with high bit set are not supported"), + // which permanently stopped the event follower. + sizes := map[common.Hash]uint64{ + common.HexToHash("0x01"): 7, + common.HexToHash("0x02"): 1<<63 - 1, + common.HexToHash("0x03"): 1 << 63, + common.HexToHash("0x04"): math.MaxUint64, + } + + BeforeEach(func() { + iterator := pusher.NewPushIterator() + owner := common.HexToAddress("0x1234567890123456789012345678901234567890") + + block := events.Block{Number: 100} + opIndex := uint64(0) + for key, size := range sizes { + block.Operations = append(block.Operations, events.Operation{ + OpIndex: opIndex, + Create: &events.OPCreate{ + Key: key, + ContentType: "application/json", + BTL: 1000, + Owner: owner, + Content: []byte(`{}`), + StringAttributes: map[string]string{}, + NumericAttributes: map[string]uint64{"size": size}, + }, + }) + opIndex++ + } + + go func() { + defer GinkgoRecover() + iterator.Push(ctx, events.BlockBatch{Blocks: []events.Block{block}}) + iterator.Close() + }() + + err := sqlStore.FollowEvents(ctx, arkivevents.BatchIterator(iterator.Iterator())) + Expect(err).NotTo(HaveOccurred()) + }) + + keysForBitmaps := func(q *store.Queries, bitmaps []*store.Bitmap) []common.Hash { + combined := store.NewBitmap() + for _, bm := range bitmaps { + combined.Or(bm.Bitmap) + } + payloads, err := q.RetrievePayloads(ctx, combined.ToArray()) + Expect(err).NotTo(HaveOccurred()) + keys := []common.Hash{} + for _, p := range payloads { + keys = append(keys, common.BytesToHash(p.EntityKey)) + } + return keys + } + + It("should find each entity by equality on its exact value", func() { + err := sqlStore.ReadTransaction(ctx, func(q *store.Queries) error { + for key, size := range sizes { + bitmap, err := q.EvaluateNumericAttributeValueEqual(ctx, store.EvaluateNumericAttributeValueEqualParams{ + Name: "size", + Value: store.NumericValue(size), + }) + Expect(err).NotTo(HaveOccurred(), "equality lookup for %d", size) + Expect(bitmap).NotTo(BeNil()) + Expect(keysForBitmaps(q, []*store.Bitmap{bitmap})).To(Equal([]common.Hash{key})) + + payloads, err := q.RetrievePayloads(ctx, bitmap.ToArray()) + Expect(err).NotTo(HaveOccurred()) + Expect(payloads).To(HaveLen(1)) + Expect(payloads[0].NumericAttributes.Values["size"]).To(Equal(size)) + } + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should order values of 2^63 or more above smaller values in range queries", func() { + err := sqlStore.ReadTransaction(ctx, func(q *store.Queries) error { + // size > 7 must include the whole high band + bitmaps, err := q.EvaluateNumericAttributeValueGreaterThan(ctx, store.EvaluateNumericAttributeValueGreaterThanParams{ + Name: "size", + Value: 7, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(keysForBitmaps(q, bitmaps)).To(ConsistOf( + common.HexToHash("0x02"), common.HexToHash("0x03"), common.HexToHash("0x04"), + )) + + // size >= 2^63 must select exactly the high band + bitmaps, err = q.EvaluateNumericAttributeValueGreaterOrEqualThan(ctx, store.EvaluateNumericAttributeValueGreaterOrEqualThanParams{ + Name: "size", + Value: 1 << 63, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(keysForBitmaps(q, bitmaps)).To(ConsistOf( + common.HexToHash("0x03"), common.HexToHash("0x04"), + )) + + // size < 2^63 must NOT include the high band + bitmaps, err = q.EvaluateNumericAttributeValueLowerThan(ctx, store.EvaluateNumericAttributeValueLowerThanParams{ + Name: "size", + Value: 1 << 63, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(keysForBitmaps(q, bitmaps)).To(ConsistOf( + common.HexToHash("0x01"), common.HexToHash("0x02"), + )) + + // size <= 2^64-1 must include everything + bitmaps, err = q.EvaluateNumericAttributeValueLessOrEqualThan(ctx, store.EvaluateNumericAttributeValueLessOrEqualThanParams{ + Name: "size", + Value: math.MaxUint64, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(keysForBitmaps(q, bitmaps)).To(HaveLen(4)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should support huge values through the query language", func() { + // Same path as the arkiv_query RPC: parse a query string, then + // evaluate it against the store. + for queryStr, expected := range map[string][]common.Hash{ + `size = 18446744073709551615`: {common.HexToHash("0x04")}, + `size > 7`: {common.HexToHash("0x02"), common.HexToHash("0x03"), common.HexToHash("0x04")}, + `size < 9223372036854775808`: {common.HexToHash("0x01"), common.HexToHash("0x02")}, + } { + ast, err := query.Parse(queryStr) + Expect(err).NotTo(HaveOccurred(), "parsing %q", queryStr) + + err = sqlStore.ReadTransaction(ctx, func(q *store.Queries) error { + bitmap, err := ast.Evaluate(ctx, q) + Expect(err).NotTo(HaveOccurred(), "evaluating %q", queryStr) + Expect(keysForBitmaps(q, []*store.Bitmap{{Bitmap: bitmap}})).To(ConsistOf(expected), "results of %q", queryStr) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("should find huge values via IN lookups", func() { + err := sqlStore.ReadTransaction(ctx, func(q *store.Queries) error { + bitmaps, err := q.EvaluateNumericAttributeValueInclusion(ctx, store.EvaluateNumericAttributeValueInclusionParams{ + Name: "size", + Values: store.NumericValues([]uint64{math.MaxUint64, 7, 42}), + }) + Expect(err).NotTo(HaveOccurred()) + Expect(keysForBitmaps(q, bitmaps)).To(ConsistOf( + common.HexToHash("0x01"), common.HexToHash("0x04"), + )) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("migration of numeric values from the raw encoding", func() { + It("should re-encode values written before the bias encoding", func() { + // Build a database as it existed before migration 000002: schema + // version 1 with numeric values stored raw. + oldDBPath := filepath.Join(tmpDir, "old.db") + db, err := sql.Open("sqlite3", "file:"+oldDBPath+"?mode=rwc") + Expect(err).NotTo(HaveOccurred()) + + sourceDriver, err := iofs.New(store.Migrations, "schema") + Expect(err).NotTo(HaveOccurred()) + dbDriver, err := migratesqlite3.WithInstance(db, &migratesqlite3.Config{}) + Expect(err).NotTo(HaveOccurred()) + m, err := migrate.NewWithInstance("iofs", sourceDriver, "sqlite3", dbDriver) + Expect(err).NotTo(HaveOccurred()) + Expect(m.Migrate(1)).To(Succeed()) + + bitmap := store.NewBitmap() + bitmap.Add(1) + _, err = db.Exec( + `INSERT INTO numeric_attributes_values_bitmaps (name, value, bitmap) VALUES (?, ?, ?)`, + "version", int64(42), bitmap, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(db.Close()).To(Succeed()) + + // Opening the store runs the remaining migrations. + migratedStore, err := sqlitebitmapstore.NewSQLiteStore(logger, oldDBPath, 1) + Expect(err).NotTo(HaveOccurred()) + defer migratedStore.Close() + + err = migratedStore.ReadTransaction(ctx, func(q *store.Queries) error { + found, err := q.EvaluateNumericAttributeValueEqual(ctx, store.EvaluateNumericAttributeValueEqualParams{ + Name: "version", + Value: 42, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(found).NotTo(BeNil()) + Expect(found.ToArray()).To(Equal([]uint64{1})) + + bitmaps, err := q.EvaluateNumericAttributeValueGreaterThan(ctx, store.EvaluateNumericAttributeValueGreaterThanParams{ + Name: "version", + Value: 40, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(bitmaps).To(HaveLen(1)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + }) }) diff --git a/store/evals.sql.go b/store/evals.sql.go index 2d10e04..a592b20 100644 --- a/store/evals.sql.go +++ b/store/evals.sql.go @@ -45,7 +45,7 @@ WHERE name = ?1 AND value = ?2 type EvaluateNumericAttributeValueEqualParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueEqual(ctx context.Context, arg EvaluateNumericAttributeValueEqualParams) (*Bitmap, error) { @@ -62,7 +62,7 @@ WHERE name = ?1 AND value >= ?2 type EvaluateNumericAttributeValueGreaterOrEqualThanParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueGreaterOrEqualThan(ctx context.Context, arg EvaluateNumericAttributeValueGreaterOrEqualThanParams) ([]*Bitmap, error) { @@ -95,7 +95,7 @@ WHERE name = ?1 AND value > ?2 type EvaluateNumericAttributeValueGreaterThanParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueGreaterThan(ctx context.Context, arg EvaluateNumericAttributeValueGreaterThanParams) ([]*Bitmap, error) { @@ -128,7 +128,7 @@ WHERE name = ?1 AND value IN (/*SLICE:values*/?) type EvaluateNumericAttributeValueInclusionParams struct { Name string - Values []uint64 + Values []NumericValue } func (q *Queries) EvaluateNumericAttributeValueInclusion(ctx context.Context, arg EvaluateNumericAttributeValueInclusionParams) ([]*Bitmap, error) { @@ -172,7 +172,7 @@ WHERE name = ?1 AND value <= ?2 type EvaluateNumericAttributeValueLessOrEqualThanParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueLessOrEqualThan(ctx context.Context, arg EvaluateNumericAttributeValueLessOrEqualThanParams) ([]*Bitmap, error) { @@ -205,7 +205,7 @@ WHERE name = ?1 AND value < ?2 type EvaluateNumericAttributeValueLowerThanParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueLowerThan(ctx context.Context, arg EvaluateNumericAttributeValueLowerThanParams) ([]*Bitmap, error) { @@ -238,7 +238,7 @@ WHERE name = ?1 AND value != ?2 type EvaluateNumericAttributeValueNotEqualParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) EvaluateNumericAttributeValueNotEqual(ctx context.Context, arg EvaluateNumericAttributeValueNotEqualParams) ([]*Bitmap, error) { @@ -271,7 +271,7 @@ WHERE name = ?1 AND value NOT IN (/*SLICE:values*/?) type EvaluateNumericAttributeValueNotInclusionParams struct { Name string - Values []uint64 + Values []NumericValue } func (q *Queries) EvaluateNumericAttributeValueNotInclusion(ctx context.Context, arg EvaluateNumericAttributeValueNotInclusionParams) ([]*Bitmap, error) { diff --git a/store/models.go b/store/models.go index c6b351f..79a5069 100644 --- a/store/models.go +++ b/store/models.go @@ -11,7 +11,7 @@ type LastBlock struct { type NumericAttributesValuesBitmap struct { Name string - Value uint64 + Value NumericValue Bitmap *Bitmap } diff --git a/store/numeric_value.go b/store/numeric_value.go new file mode 100644 index 0000000..a73c9e7 --- /dev/null +++ b/store/numeric_value.go @@ -0,0 +1,50 @@ +package store + +import ( + "database/sql/driver" + "fmt" +) + +// numericValueBias is XORed into a numeric attribute value before it is +// bound to SQL and after it is read back. +const numericValueBias = uint64(1) << 63 + +// NumericValue is a uint64 numeric attribute value as stored in SQLite. +// +// SQLite integers are signed 64-bit and Go's database/sql refuses to bind +// a uint64 with the high bit set (values of 2^63 or more), which used to +// make such values fatal to the event follower: +// +// sql: converting argument $2 type: uint64 values with high bit set are not supported +// +// The value is stored with its top bit flipped (equivalent to subtracting +// 2^63), mapping [0, 2^64) one-to-one onto [-2^63, 2^63) while preserving +// order. Unsigned ordering therefore matches SQLite's signed INTEGER +// ordering, so range comparisons (<, <=, >, >=) stay correct across the +// full uint64 range. Existing rows written with the previous raw encoding +// are converted by migration 000002. +type NumericValue uint64 + +// NumericValues converts a slice of raw uint64 values to NumericValue. +func NumericValues(vs []uint64) []NumericValue { + out := make([]NumericValue, len(vs)) + for i, v := range vs { + out[i] = NumericValue(v) + } + return out +} + +// Valuer interface for writing to DB +func (v NumericValue) Value() (driver.Value, error) { + return int64(uint64(v) ^ numericValueBias), nil +} + +// Scanner interface for reading from DB +func (v *NumericValue) Scan(src any) error { + i, ok := src.(int64) + if !ok { + return fmt.Errorf("expected int64, got %T", src) + } + *v = NumericValue(uint64(i) ^ numericValueBias) + return nil +} diff --git a/store/queries.sql.go b/store/queries.sql.go index 0788eb6..fc0ef1d 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -16,7 +16,7 @@ WHERE name = ? AND value = ? type DeleteNumericAttributeValueBitmapParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) DeleteNumericAttributeValueBitmap(ctx context.Context, arg DeleteNumericAttributeValueBitmapParams) error { @@ -67,7 +67,7 @@ WHERE name = ? AND value = ? type GetNumericAttributeValueBitmapParams struct { Name string - Value uint64 + Value NumericValue } func (q *Queries) GetNumericAttributeValueBitmap(ctx context.Context, arg GetNumericAttributeValueBitmapParams) (*Bitmap, error) { @@ -142,7 +142,7 @@ ON CONFLICT (name, value) DO UPDATE SET bitmap = excluded.bitmap type UpsertNumericAttributeValueBitmapParams struct { Name string - Value uint64 + Value NumericValue Bitmap *Bitmap } diff --git a/store/schema/000002_rebias_numeric_values.up.sql b/store/schema/000002_rebias_numeric_values.up.sql new file mode 100644 index 0000000..d3f15f4 --- /dev/null +++ b/store/schema/000002_rebias_numeric_values.up.sql @@ -0,0 +1,10 @@ +-- Numeric attribute values are now stored with their top bit flipped +-- (value - 2^63) so that the full uint64 range fits in SQLite's signed +-- INTEGER while preserving order; see store/numeric_value.go. +-- +-- Existing rows hold the raw value, which is always < 2^63 (larger values +-- could not be written before this encoding existed), so re-encoding is a +-- plain subtraction. The literal is split in two because SQLite parses +-- 9223372036854775808 as a REAL, which would corrupt the values. +UPDATE numeric_attributes_values_bitmaps +SET value = value - 9223372036854775807 - 1; diff --git a/store/sqlc.yaml b/store/sqlc.yaml index 47aa2ae..31eb4b6 100644 --- a/store/sqlc.yaml +++ b/store/sqlc.yaml @@ -24,8 +24,11 @@ sql: go_type: type: "NumericAttributes" pointer: true + # Stored top-bit-flipped so the full uint64 range fits SQLite's + # signed INTEGER in order-preserving fashion; see numeric_value.go. - column: "numeric_attributes_values_bitmaps.value" - go_type: "uint64" + go_type: + type: "NumericValue" - column: "string_attributes_values_bitmaps.bitmap" go_type: type: "Bitmap"