substreams-database-change 4.0.0

Substreams database change containg helpers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# Implementation Plan: NumericAddable Trait for add/sub Operations

## Overview

Implement a `NumericAddable` trait that uses the AddAssign pattern to optimize `add()` and `sub()` operations by mutating a BigDecimal directly, reducing allocations from 4 to 2 per operation.

## Current Implementation Issues

**Location:** [src/tables.rs:410-498](src/tables.rs#L410-L498)

The current `add()` and `sub()` methods:
- Accept `<T: ToDatabaseValue>` (any type that converts to string)
- Convert ALL values to BigDecimal for computation
- Create 4 allocations per operation:
  1. Parse existing value → BigDecimal
  2. Parse new value → BigDecimal
  3. Add BigDecimals (intermediate result)
  4. Convert result to string

**Performance bottleneck:** Simple integer counters (common in blockchain data) pay the full BigDecimal allocation cost every time.

## Design: NumericAddable Trait

### Trait Definition

```rust
/// Trait for types that can be added to a BigDecimal in-place
pub trait NumericAddable {
    /// Add self to the target BigDecimal, mutating it directly
    /// This enables efficient accumulation without intermediate allocations
    fn add_assign_to(&self, target: &mut BigDecimal);

    /// Subtract self from the target BigDecimal, mutating it directly
    /// Equivalent to: *target -= self
    fn sub_assign_from(&self, target: &mut BigDecimal);
}
```

### Key Characteristics

- **No base trait required** - stands alone, no dependency on `NumericValue`
- **AddAssign pattern** - mutates existing BigDecimal instead of creating new ones
- **Simple API** - two methods that directly express the operation
- **Type agnostic** - works for integers, BigDecimal, and strings uniformly

## Implementation Details

### For Integral Types (i8, i16, i32, i64, u8, u16, u32, u64)

Use a macro to implement `NumericAddable` for all integral types uniformly:

```rust
macro_rules! impl_numeric_addable_for_integer {
    ($($t:ty),*) => {
        $(
            impl NumericAddable for $t {
                fn add_assign_to(&self, target: &mut BigDecimal) {
                    *target += BigDecimal::from(*self);
                }

                fn sub_assign_from(&self, target: &mut BigDecimal) {
                    *target -= BigDecimal::from(*self);
                }
            }
        )*
    };
}

// Apply to all integer types
impl_numeric_addable_for_integer!(i8, i16, i32, i64, u8, u16, u32, u64);
```

**Performance:** Convert integer → BigDecimal once, add directly to mutable target.

**Note:** Unsigned subtraction can produce negative results (valid for database context).

### For BigDecimal, BigInt and their references

Use a macro to implement `NumericAddable` for BigDecimal/BigInt types:

```rust
macro_rules! impl_numeric_addable_for_bignum {
    ($($t:ty),*) => {
        $(
            impl NumericAddable for $t {
                fn add_assign_to(&self, target: &mut BigDecimal) {
                    *target += self;
                }

                fn sub_assign_from(&self, target: &mut BigDecimal) {
                    *target -= self;
                }
            }
        )*
    };
}

// Apply to BigDecimal and BigInt types (both owned and references)
impl_numeric_addable_for_bignum!(BigDecimal, &BigDecimal, BigInt, &BigInt);
```

**Performance:** Direct BigDecimal operations, no conversion needed.

**Note:** This works because `BigDecimal` implements `AddAssign` and `SubAssign` for both owned and reference types.

### For String and &str

```rust
impl NumericAddable for String {
    fn add_assign_to(&self, target: &mut BigDecimal) {
        let value = BigDecimal::from_str(self)
            .expect("String must contain valid numeric value");
        *target += value;
    }

    fn sub_assign_from(&self, target: &mut BigDecimal) {
        let value = BigDecimal::from_str(self)
            .expect("String must contain valid numeric value");
        *target -= value;
    }
}

impl NumericAddable for &str {
    fn add_assign_to(&self, target: &mut BigDecimal) {
        let value = BigDecimal::from_str(self)
            .expect("&str must contain valid numeric value");
        *target += value;
    }

    fn sub_assign_from(&self, target: &mut BigDecimal) {
        let value = BigDecimal::from_str(self)
            .expect("&str must contain valid numeric value");
        *target -= value;
    }
}
```

**Performance cost:** Parse string to BigDecimal (acceptable per requirements).

### For BigInt (if needed)

```rust
impl NumericAddable for BigInt {
    fn add_assign_to(&self, target: &mut BigDecimal) {
        // Convert BigInt to BigDecimal first
        let value = BigDecimal::from_str(&self.to_string())
            .expect("BigInt should convert to valid BigDecimal");
        *target += value;
    }

    fn sub_assign_from(&self, target: &mut BigDecimal) {
        let value = BigDecimal::from_str(&self.to_string())
            .expect("BigInt should convert to valid BigDecimal");
        *target -= value;
    }
}
```

## Updated Row Methods

### Update add() method signature

**File:** [src/tables.rs:413](src/tables.rs#L413)

```rust
// OLD
pub fn add<T: ToDatabaseValue>(&mut self, name: &str, value: T) -> &mut Self

// NEW
pub fn add<T: NumericAddable>(&mut self, name: &str, value: T) -> &mut Self {
    if self.operation == Operation::Delete {
        panic!("cannot set fields on a delete operation")
    }
    self.accumulate_add(name, value, false);
    self
}
```

### Update sub() method signature

**File:** [src/tables.rs:424](src/tables.rs#L424)

```rust
// OLD
pub fn sub<T: ToDatabaseValue>(&mut self, name: &str, value: T) -> &mut Self

// NEW
pub fn sub<T: NumericAddable>(&mut self, name: &str, value: T) -> &mut Self {
    if self.operation == Operation::Delete {
        panic!("cannot set fields on a delete operation")
    }
    self.accumulate_add(name, value, true);
    self
}
```

### Refactor accumulate_add() implementation

**File:** [src/tables.rs:437-498](src/tables.rs#L437-L498)

**Current complexity:** 62 lines with string manipulation and BigDecimal parsing

**New implementation:**

```rust
fn accumulate_add<T: NumericAddable>(
    &mut self,
    name: &str,
    value: T,
    negate: bool,
) {
    match self.columns.get_mut(name) {
        Some(existing) => {
            // Validate operation compatibility
            match existing.update_op {
                UpdateOp::Unspecified => panic!(
                    "cannot call add/sub() on field '{}' after unspecified - incompatible operations",
                    name
                ),
                UpdateOp::Set | UpdateOp::Add => {
                    // Parse existing value to BigDecimal once
                    let mut target = BigDecimal::from_str(&existing.value)
                        .expect("existing value should be valid BigDecimal");

                    // Mutate BigDecimal directly using trait method
                    if negate {
                        value.sub_assign_from(&mut target);
                    } else {
                        value.add_assign_to(&mut target);
                    }

                    // Convert back to string for storage
                    existing.value = target.to_string();
                    // Keep existing op (Set stays Set, Add stays Add)
                }
                UpdateOp::Max => panic!(
                    "cannot call add/sub() on field '{}' after max() - incompatible operations",
                    name
                ),
                UpdateOp::Min => panic!(
                    "cannot call add/sub() on field '{}' after min() - incompatible operations",
                    name
                ),
                UpdateOp::SetIfNull => panic!(
                    "cannot call add/sub() on field '{}' after set_if_null() - incompatible operations",
                    name
                ),
            }
        }
        None => {
            // No existing value - create new entry starting from 0
            let mut target = BigDecimal::from_str("0").unwrap();

            if negate {
                value.sub_assign_from(&mut target);
            } else {
                value.add_assign_to(&mut target);
            }

            self.columns.insert(
                name.to_string(),
                FieldValue::with_op(target.to_string(), UpdateOp::Add),
            );
        }
    }
}
```

**Improvements:**
- Reduced from ~62 lines to ~45 lines
- Eliminated string manipulation for negation
- Clearer logic flow
- 2 allocations instead of 4

## File Structure

### New files to create

1. **src/numeric/traits.rs** - Trait definition
   ```rust
   use substreams::scalar::BigDecimal;
   use std::str::FromStr;

   pub trait NumericAddable {
       fn add_assign_to(&self, target: &mut BigDecimal);
       fn sub_assign_from(&self, target: &mut BigDecimal);
   }
   ```

2. **src/numeric/impls.rs** - All type implementations
   ```rust
   use substreams::scalar::{BigDecimal, BigInt};
   use std::str::FromStr;
   use crate::numeric::NumericAddable;

   // Macro for implementing NumericAddable on all integer types
   macro_rules! impl_numeric_addable_for_integer {
       ($($t:ty),*) => {
           $(
               impl NumericAddable for $t {
                   fn add_assign_to(&self, target: &mut BigDecimal) {
                       *target += BigDecimal::from(*self);
                   }

                   fn sub_assign_from(&self, target: &mut BigDecimal) {
                       *target -= BigDecimal::from(*self);
                   }
               }
           )*
       };
   }

   // Apply to all integer types
   impl_numeric_addable_for_integer!(i8, i16, i32, i64, u8, u16, u32, u64);

   // Macro for implementing NumericAddable on BigDecimal/BigInt types
   macro_rules! impl_numeric_addable_for_bignum {
       ($($t:ty),*) => {
           $(
               impl NumericAddable for $t {
                   fn add_assign_to(&self, target: &mut BigDecimal) {
                       *target += self;
                   }

                   fn sub_assign_from(&self, target: &mut BigDecimal) {
                       *target -= self;
                   }
               }
           )*
       };
   }

   // Apply to BigDecimal and BigInt types
   impl_numeric_addable_for_bignum!(BigDecimal, &BigDecimal, BigInt, &BigInt);

   // Implementations for String, &str
   // (see detailed examples in "Implementation Details" section)
   ```

3. **src/numeric/mod.rs** - Module exports
   ```rust
   mod traits;
   mod impls;

   pub use traits::NumericAddable;
   ```

4. **src/numeric/tests.rs** - Unit tests
   - Test AddAssign mutation works correctly
   - Test SubAssign mutation works correctly
   - Test type mixing (i64 → String → i64)
   - Test large value precision
   - Test negative results from subtraction

### Files to update

1. **src/lib.rs** - Add module
   ```rust
   pub mod numeric;  // Add this line
   pub mod tables;
   // ... existing modules
   ```

2. **src/tables.rs** - Update imports and methods
   ```rust
   use crate::numeric::NumericAddable;  // Add import

   // Update method signatures (lines ~413, ~424)
   // Update accumulate_add implementation (lines ~437-498)
   ```

## Implementation Steps

1. **Create module structure**
   - Create `src/numeric/` directory
   - Create `traits.rs`, `impls.rs`, `mod.rs`, `tests.rs`
   - Update `src/lib.rs` to add `pub mod numeric;`

2. **Define NumericAddable trait**
   - Write trait in `traits.rs`
   - Add necessary imports

3. **Implement for i64 first** (validate design)
   - Write implementation in `impls.rs`
   - Add basic unit test in `tests.rs`

4. **Implement for all integer types using macro**
   - Use `impl_numeric_addable_for_integer!` macro
   - Single implementation covers: i8, i16, i32, i64, u8, u16, u32, u64

5. **Implement for BigDecimal/BigInt types using macro**
   - Use `impl_numeric_addable_for_bignum!` macro
   - Single implementation covers: BigDecimal, &BigDecimal, BigInt, &BigInt

6. **Implement for String types**
   - String, &str (requires explicit parsing, cannot use macro)

5. **Update Row methods**
   - Change `add<T>` signature to use `NumericAddable`
   - Change `sub<T>` signature to use `NumericAddable`
   - Refactor `accumulate_add()` to use trait methods

6. **Run existing tests**
   - All tests in [src/tables.rs:926-1582]src/tables.rs#L926-L1582 must pass
   - Critical tests:
     - `add_plus_add_accumulates_keeps_add_op` (line 1013)
     - `set_plus_add_accumulates_keeps_set_op` (line 1066)
     - `bigdecimal_precision_preserved` (line 1455)

7. **Add comprehensive new tests**
   - AddAssign behavior verification
   - Type mixing scenarios
   - Edge cases (negative values, zero, large numbers)

## Edge Cases & Testing

### Type Mixing

**Scenario:** User calls `row.add("balance", 100i64)` then `row.add("balance", "0.5")`

**Behavior:**
1. First call: `target = BigDecimal::from_str("0")`, add 100 → stores `"100"`
2. Second call: `target = BigDecimal::from_str("100")`, add "0.5" → stores `"100.5"`
3. Seamless mixing, no special handling needed

### Subtraction with Unsigned Types

**Scenario:** `row.sub("balance", 100u64)` when balance is 50

**Behavior:**
1. Parse "50" as BigDecimal
2. Call `100u64.sub_assign_from(&mut target)``target = 50 - 100 = -50`
3. Stores `"-50"` (valid in database context)

### Large Values

**Scenario:** Blockchain wei values (10^18) or larger

**Behavior:**
- All operations go through BigDecimal
- No precision loss, exact arithmetic
- Works identically to current implementation

### Empty Column (first operation)

**Scenario:** First `add()` call on a new column

**Behavior:**
1. No existing entry in `self.columns`
2. Create `target = BigDecimal::from_str("0")`
3. Add value to target
4. Insert new FieldValue with Add operation

## Performance Impact

**Current approach:**
- Parse existing value → BigDecimal (allocation 1)
- Parse new value → BigDecimal (allocation 2)
- Add BigDecimals (allocation 3)
- Convert result to string (allocation 4)

**Optimized approach:**
- Parse existing value → BigDecimal (allocation 1)
- Call `value.add_assign_to(&mut bigdecimal)` - mutates in place
- Convert result to string (allocation 2)

**Result:** ~50% reduction in allocations (4 → 2)

**Real-world impact:** For Substreams processing 1000s of events per block with counter updates:
- Approximately 2x fewer allocations
- Reduced memory pressure
- Faster execution for accumulation-heavy workloads

## Success Criteria

- ✅ All existing tests pass without modification
- ✅ Compile-time type safety (non-numeric types rejected at compile time)
- ✅ Performance improvement: 2 allocations instead of 4
- ✅ Precision maintained for all numeric operations
- ✅ String types accepted with proper validation
- ✅ Type mixing works seamlessly (i64 → String → i64 sequences)
- ✅ Clean API - no breaking changes to public interface
- ✅ Code is simpler and more maintainable than current implementation