-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathdata_source.v
More file actions
779 lines (725 loc) · 20.2 KB
/
data_source.v
File metadata and controls
779 lines (725 loc) · 20.2 KB
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
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
module gui
import rand
import time
pub enum GridPaginationKind as u8 {
cursor
offset
}
pub enum GridMutationKind as u8 {
create
update
delete
}
@[minify]
pub struct GridCursorPageReq {
pub:
cursor string
limit int = 100
}
@[minify]
pub struct GridOffsetPageReq {
pub:
start_index int
end_index int
}
pub type GridPageRequest = GridCursorPageReq | GridOffsetPageReq
// GridAbortSignal communicates cancellation from the main
// thread to a spawned goroutine. `aborted` is a plain bool
// (not atomic) — the stale-response request_id guard in
// apply_success catches races, so a missed cancellation only
// wastes work rather than causing incorrect state.
@[heap; minify]
pub struct GridAbortSignal {
mut:
aborted bool
}
// is_aborted reports cancellation status.
pub fn (signal &GridAbortSignal) is_aborted() bool {
if isnil(signal) {
return false
}
return signal.aborted
}
fn (mut signal GridAbortSignal) set_aborted(value bool) {
signal.aborted = value
}
@[heap; minify]
pub struct GridAbortController {
pub mut:
signal &GridAbortSignal = unsafe { nil }
}
// new_grid_abort_controller allocates a fresh abort controller.
pub fn new_grid_abort_controller() &GridAbortController {
signal := &GridAbortSignal{}
return &GridAbortController{
signal: signal
}
}
// abort marks request as cancelled.
pub fn (mut controller GridAbortController) abort() {
if isnil(controller.signal) {
return
}
controller.signal.set_aborted(true)
}
@[minify]
pub struct GridDataRequest {
pub:
grid_id string
query GridQueryState
page GridPageRequest
signal &GridAbortSignal = unsafe { nil }
request_id u64
}
@[minify]
pub struct GridDataResult {
pub:
rows []GridRow
next_cursor string
prev_cursor string
row_count ?int
has_more bool
received_count int
}
@[minify]
pub struct GridDataCapabilities {
pub:
supports_cursor_pagination bool = true
supports_offset_pagination bool
supports_numbered_pages bool
row_count_known bool
supports_create bool
supports_update bool
supports_delete bool
supports_batch_delete bool
}
@[minify]
pub struct GridMutationRequest {
pub:
grid_id string
kind GridMutationKind
query GridQueryState
rows []GridRow
row_ids []string
edits []GridCellEdit
signal &GridAbortSignal = unsafe { nil }
request_id u64
}
@[minify]
pub struct GridMutationResult {
pub:
created []GridRow
updated []GridRow
deleted_ids []string
failed_ids []string // row IDs that failed in batch ops
errors map[string]string // row_id -> error message
row_count ?int
}
const data_grid_source_max_page_limit = 10000
// FNV-1a 64-bit constants (hash.fnv1a keeps these private).
const data_grid_fnv64_offset = u64(14695981039346656037)
const data_grid_fnv64_prime = u64(1099511628211)
pub interface DataGridDataSource {
capabilities() GridDataCapabilities
fetch_data(req GridDataRequest) !GridDataResult
mut:
mutate_data(req GridMutationRequest) !GridMutationResult
}
@[heap; minify]
pub struct InMemoryDataSource {
pub mut:
rows []GridRow
pub:
default_limit int = 100
latency_ms int
row_count_known bool = true
supports_cursor bool = true
supports_offset bool = true
}
pub fn (source InMemoryDataSource) capabilities() GridDataCapabilities {
return GridDataCapabilities{
supports_cursor_pagination: source.supports_cursor
supports_offset_pagination: source.supports_offset
supports_numbered_pages: source.supports_offset
row_count_known: source.row_count_known
supports_create: true
supports_update: true
supports_delete: true
supports_batch_delete: true
}
}
pub fn (source InMemoryDataSource) fetch_data(req GridDataRequest) !GridDataResult {
return data_grid_source_inmemory_fetch(source.rows, source.default_limit, source.latency_ms,
source.row_count_known, req)
}
pub fn (mut source InMemoryDataSource) mutate_data(req GridMutationRequest) !GridMutationResult {
return data_grid_source_inmemory_mutate(mut source.rows, source.latency_ms, source.row_count_known,
req)
}
fn data_grid_source_inmemory_fetch(rows []GridRow, default_limit int, latency_ms int, row_count_known bool, req GridDataRequest) !GridDataResult {
data_grid_source_sleep_with_abort(req.signal, latency_ms)!
filtered := data_grid_source_apply_query(rows, req.query)
limit := int_clamp(if default_limit > 0 { default_limit } else { 100 }, 1, data_grid_source_max_page_limit)
start, end := match req.page {
GridCursorPageReq {
s := int_clamp(data_grid_source_cursor_to_index(req.page.cursor), 0, filtered.len)
chunk := int_clamp(if req.page.limit > 0 { req.page.limit } else { limit },
1, data_grid_source_max_page_limit)
s, int_min(filtered.len, s + chunk)
}
GridOffsetPageReq {
data_grid_source_offset_bounds(req.page.start_index, req.page.end_index, filtered.len,
limit)
}
}
page := filtered[start..end].clone()
grid_abort_check(req.signal)!
is_cursor := req.page is GridCursorPageReq
return GridDataResult{
rows: page
next_cursor: if is_cursor && end < filtered.len {
data_grid_source_cursor_from_index(end)
} else {
''
}
prev_cursor: if is_cursor {
data_grid_source_prev_cursor(start, end - start)
} else {
''
}
row_count: if row_count_known { ?int(filtered.len) } else { none }
has_more: end < filtered.len
received_count: page.len
}
}
fn data_grid_source_inmemory_mutate(mut rows []GridRow, latency_ms int, row_count_known bool, req GridMutationRequest) !GridMutationResult {
data_grid_source_sleep_with_abort(req.signal, latency_ms)!
mut work := rows.clone()
result := data_grid_source_apply_mutation(mut work, req.kind, req.rows, req.row_ids,
req.edits)!
grid_abort_check(req.signal)!
// V requires `unsafe` to fully reassign a `mut` array param.
rows = unsafe { work }
return GridMutationResult{
created: result.created
updated: result.updated
deleted_ids: result.deleted_ids
row_count: if row_count_known { ?int(rows.len) } else { none }
}
}
// data_grid_source_offset_bounds clamps start/end to [0,total]
// and falls back to default_limit when the range is empty.
fn data_grid_source_offset_bounds(start_index int, end_index int, total int, default_limit int) (int, int) {
start := int_clamp(start_index, 0, total)
mut end := int_clamp(end_index, start, total)
if end <= start {
end = int_min(total, start + default_limit)
}
return start, end
}
fn grid_abort_check(signal &GridAbortSignal) ! {
if signal.is_aborted() {
return error('grid: request aborted')
}
}
fn data_grid_source_sleep_with_abort(signal &GridAbortSignal, ms int) ! {
if ms <= 0 {
grid_abort_check(signal)!
return
}
mut remaining := ms
for remaining > 0 {
grid_abort_check(signal)!
step := int_min(remaining, 20)
time.sleep(step * time.millisecond)
remaining -= step
}
grid_abort_check(signal)!
}
fn data_grid_source_cursor_from_index(index int) string {
return 'i:${int_max(0, index)}'
}
fn data_grid_source_prev_cursor(start int, page_size int) string {
if start <= 0 {
return ''
}
return data_grid_source_cursor_from_index(int_max(0, start - page_size))
}
fn data_grid_source_cursor_to_index(cursor string) int {
if idx := data_grid_source_cursor_to_index_opt(cursor) {
return idx
}
return 0
}
fn data_grid_source_cursor_to_index_opt(cursor string) ?int {
trimmed := cursor.trim_space()
if trimmed.len == 0 {
return ?int(0)
}
if trimmed.starts_with('i:') {
val := trimmed[2..]
if !data_grid_source_is_decimal(val) {
return none
}
return ?int(int_max(0, val.int()))
}
if !data_grid_source_is_decimal(trimmed) {
return none
}
return ?int(int_max(0, trimmed.int()))
}
fn data_grid_source_is_decimal(input string) bool {
if input.len == 0 {
return false
}
for ch in input {
if ch < `0` || ch > `9` {
return false
}
}
return true
}
// data_grid_source_apply_query filters and sorts rows in memory.
// NOTE: returns `rows` directly (slice alias, not clone) when
// no filters/sorts apply. Callers must clone if mutation is
// intended.
fn data_grid_source_apply_query(rows []GridRow, query GridQueryState) []GridRow {
if query.quick_filter.len == 0 && query.filters.len == 0 && query.sorts.len == 0 {
return rows
}
has_filters := query.quick_filter.len > 0 || query.filters.len > 0
filtered := if has_filters {
needle := query.quick_filter.to_lower()
mut lowered_filters := []GridFilterLowered{cap: query.filters.len}
for filter in query.filters {
lowered_filters << GridFilterLowered{
col_id: filter.col_id
op: filter.op
value: filter.value.to_lower()
}
}
rows.filter(data_grid_source_row_matches_query(it, needle, lowered_filters))
} else {
rows
}
if query.sorts.len == 0 {
return filtered
}
// Sorting is lexicographic (string comparison). Numeric-aware
// sorting is the caller's responsibility via custom data sources.
n := filtered.len
if n <= 1 {
return filtered
}
mut idxs := []int{len: n, init: index}
if query.sorts.len == 1 {
// Single-sort fast path: one key array, no inner loop.
sort0 := query.sorts[0]
keys := []string{len: n, init: filtered[index].cells[sort0.col_id] or { '' }}
dir := if sort0.dir == .asc { 1 } else { -1 }
idxs.sort_with_compare(fn [keys, dir] (ia &int, ib &int) int {
ka := keys[*ia]
kb := keys[*ib]
if ka == kb {
return 0
}
return if ka < kb { -dir } else { dir }
})
} else {
// Multi-sort: pre-extract key columns.
sorts := query.sorts
mut key_cols := [][]string{len: sorts.len}
for si, sort in sorts {
mut col := []string{len: n}
for i, row in filtered {
col[i] = row.cells[sort.col_id] or { '' }
}
key_cols[si] = col
}
idxs.sort_with_compare(fn [sorts, key_cols] (ia &int, ib &int) int {
a := *ia
b := *ib
for si, sort in sorts {
ka := key_cols[si][a]
kb := key_cols[si][b]
if ka == kb {
continue
}
cmp := if ka < kb { -1 } else { 1 }
return if sort.dir == .asc { cmp } else { -cmp }
}
return 0
})
}
mut result := []GridRow{len: n}
for i, idx in idxs {
result[i] = filtered[idx]
}
return result
}
@[minify]
struct GridFilterLowered {
col_id string
op string
value string
}
fn data_grid_source_row_matches_query(row GridRow, needle string, filters []GridFilterLowered) bool {
if needle.len > 0 {
mut matched := false
for _, value in row.cells {
if grid_contains_lower(value, needle) {
matched = true
break
}
}
if !matched {
return false
}
}
for filter in filters {
cell := row.cells[filter.col_id] or { '' }
matched := match filter.op {
'equals' { grid_equals_lower(cell, filter.value) }
'starts_with' { grid_starts_with_lower(cell, filter.value) }
'ends_with' { grid_ends_with_lower(cell, filter.value) }
else { grid_contains_lower(cell, filter.value) }
}
if !matched {
return false
}
}
return true
}
// ASCII lowercase byte (a-z, A-Z only).
@[inline]
fn grid_lower_byte(c u8) u8 {
if c >= `A` && c <= `Z` {
return c | 0x20
}
return c
}
// grid_contains_lower checks haystack.to_lower().contains(needle)
// without allocating. `needle` must already be lowered.
fn grid_contains_lower(haystack string, needle string) bool {
if needle.len == 0 {
return true
}
if haystack.len < needle.len {
return false
}
limit := haystack.len - needle.len
// bounds: i+j <= limit+needle.len-1 == haystack.len-1
for i := 0; i <= limit; i++ {
mut found := true
for j := 0; j < needle.len; j++ {
if grid_lower_byte(unsafe { haystack.str[i + j] }) != unsafe { needle.str[j] } {
found = false
break
}
}
if found {
return true
}
}
return false
}
// grid_equals_lower checks haystack.to_lower() == needle
// without allocating. `needle` must already be lowered.
fn grid_equals_lower(haystack string, needle string) bool {
if haystack.len != needle.len {
return false
}
// bounds: i < haystack.len == needle.len
for i := 0; i < haystack.len; i++ {
if grid_lower_byte(unsafe { haystack.str[i] }) != unsafe { needle.str[i] } {
return false
}
}
return true
}
// grid_starts_with_lower checks haystack.to_lower().starts_with(needle)
// without allocating. `needle` must already be lowered.
fn grid_starts_with_lower(haystack string, needle string) bool {
if haystack.len < needle.len {
return false
}
// bounds: i < needle.len <= haystack.len
for i := 0; i < needle.len; i++ {
if grid_lower_byte(unsafe { haystack.str[i] }) != unsafe { needle.str[i] } {
return false
}
}
return true
}
// grid_ends_with_lower checks haystack.to_lower().ends_with(needle)
// without allocating. `needle` must already be lowered.
fn grid_ends_with_lower(haystack string, needle string) bool {
if haystack.len < needle.len {
return false
}
off := haystack.len - needle.len
// bounds: i+off < needle.len+off == haystack.len
for i := 0; i < needle.len; i++ {
if grid_lower_byte(unsafe { haystack.str[i + off] }) != unsafe { needle.str[i] } {
return false
}
}
return true
}
// grid_query_signature returns a stable FNV-1a 64-bit hash
// of the query state. Filters are sorted by col_id for order
// independence (AND-combined). Eliminates per-frame string
// allocation compared to the previous Builder approach.
fn grid_query_signature(query GridQueryState) u64 {
mut h := data_grid_fnv64_offset
h = data_grid_fnv64_str(h, query.quick_filter)
// Sorts: preserve order (primary/secondary priority matters).
h = data_grid_fnv64_byte(h, `|`)
h = data_grid_fnv64_byte(h, `s`)
for sort in query.sorts {
h = data_grid_fnv64_byte(h, 0x1e) // record_sep between sorts
h = data_grid_fnv64_str(h, sort.col_id)
h = data_grid_fnv64_byte(h, if sort.dir == .desc { `d` } else { `a` })
}
// Filters: sort by col_id for stable signature
// (AND-combined, so order is semantically irrelevant).
h = data_grid_fnv64_byte(h, `|`)
h = data_grid_fnv64_byte(h, `f`)
filters := query.filters
if filters.len <= 1 {
// 0 or 1 filter: no sort needed, skip index alloc.
for filter in filters {
h = grid_hash_filter(h, filter)
}
return h
}
mut idxs := []int{len: filters.len, init: index}
idxs.sort_with_compare(fn [filters] (ia &int, ib &int) int {
a := filters[*ia]
b := filters[*ib]
if a.col_id < b.col_id {
return -1
}
if a.col_id > b.col_id {
return 1
}
if a.op < b.op {
return -1
}
if a.op > b.op {
return 1
}
if a.value < b.value {
return -1
}
if a.value > b.value {
return 1
}
return 0
})
for i in idxs {
h = grid_hash_filter(h, filters[i])
}
return h
}
// Incremental FNV-1a: feed a string into running hash.
@[direct_array_access; inline]
fn data_grid_fnv64_str(h u64, s string) u64 {
mut hash := h
for i in 0 .. s.len {
hash = (hash ^ u64(s[i])) * data_grid_fnv64_prime
}
return hash
}
@[inline]
fn data_grid_fnv64_byte(h u64, b u8) u64 {
return (h ^ u64(b)) * data_grid_fnv64_prime
}
@[inline]
fn data_grid_fnv64_u64(h u64, val u64) u64 {
mut hash := (h ^ (val & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 8) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 16) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 24) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 32) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 40) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 48) & 0xff)) * data_grid_fnv64_prime
hash = (hash ^ ((val >> 56) & 0xff)) * data_grid_fnv64_prime
return hash
}
// grid_hash_filter hashes a single filter into a running
// FNV-1a hash. Used by grid_query_signature.
fn grid_hash_filter(h u64, f GridFilter) u64 {
mut hash := data_grid_fnv64_byte(h, 0x1e)
hash = data_grid_fnv64_str(hash, f.col_id)
hash = data_grid_fnv64_byte(hash, 0x1f)
hash = data_grid_fnv64_str(hash, f.op)
hash = data_grid_fnv64_byte(hash, 0x1f)
hash = data_grid_fnv64_str(hash, f.value)
return hash
}
@[minify]
struct GridMutationApplyResult {
created []GridRow
updated []GridRow
deleted_ids []string
}
fn data_grid_source_apply_mutation(mut rows []GridRow, kind GridMutationKind, req_rows []GridRow, req_row_ids []string, edits []GridCellEdit) !GridMutationApplyResult {
return match kind {
.create { data_grid_source_apply_create(mut rows, req_rows) }
.update { data_grid_source_apply_update(mut rows, req_rows, edits) }
.delete { data_grid_source_apply_delete(mut rows, req_rows, req_row_ids) }
}
}
fn data_grid_source_apply_create(mut rows []GridRow, req_rows []GridRow) !GridMutationApplyResult {
if req_rows.len == 0 {
return GridMutationApplyResult{}
}
// Build existing-id set once for O(1) lookups.
mut existing := map[string]bool{}
for idx, row in rows {
existing[data_grid_row_id(row, idx)] = true
}
mut created := []GridRow{cap: req_rows.len}
for row in req_rows {
next_id := data_grid_source_next_create_row_id(rows, existing, row.id)!
next_row := GridRow{
...row
id: next_id
cells: row.cells.clone()
}
rows << next_row
existing[next_id] = true
created << next_row
}
return GridMutationApplyResult{
created: created
}
}
fn data_grid_source_apply_update(mut rows []GridRow, req_rows []GridRow, edits []GridCellEdit) !GridMutationApplyResult {
mut updated_ids := map[string]bool{}
// Group edits by row_id for single-pass application.
mut edits_by_row := map[string][]GridCellEdit{}
for edit in edits {
if edit.row_id.len == 0 {
return error('grid: row id is required')
}
if edit.col_id.len == 0 {
return error('grid: edit has empty col id')
}
edits_by_row[edit.row_id] << edit
}
mut updated := []GridRow{cap: req_rows.len + edits_by_row.len}
// Build index map once to avoid O(n) scan per lookup.
mut row_idx := map[string]int{}
for idx, row in rows {
row_idx[data_grid_row_id(row, idx)] = idx
}
// Apply req_rows with matching edits in one clone.
for req_row in req_rows {
if req_row.id.len == 0 {
return error('grid: row id is required')
}
if idx := row_idx[req_row.id] {
mut cells := rows[idx].cells.clone()
for key, value in req_row.cells {
cells[key] = value
}
for edit in edits_by_row[req_row.id] or { []GridCellEdit{} } {
cells[edit.col_id] = edit.value
}
rows[idx] = GridRow{
...rows[idx]
cells: cells
}
updated << rows[idx]
updated_ids[req_row.id] = true
} else {
return error('grid: update row not found: ${req_row.id}')
}
}
// Apply remaining edits not covered by req_rows.
for row_id, row_edits in edits_by_row {
if updated_ids[row_id] {
continue
}
if idx := row_idx[row_id] {
mut cells := rows[idx].cells.clone()
for edit in row_edits {
cells[edit.col_id] = edit.value
}
rows[idx] = GridRow{
...rows[idx]
cells: cells
}
updated << rows[idx]
} else {
return error('grid: edit row not found: ${row_id}')
}
}
return GridMutationApplyResult{
updated: updated
}
}
fn data_grid_source_apply_delete(mut rows []GridRow, req_rows []GridRow, req_row_ids []string) !GridMutationApplyResult {
id_set := grid_deduplicate_row_ids(req_rows, req_row_ids)
if id_set.len == 0 {
return GridMutationApplyResult{}
}
mut kept := []GridRow{cap: rows.len}
mut deleted_ids := []string{cap: id_set.len}
for idx, row in rows {
row_id := data_grid_row_id(row, idx)
if id_set[row_id] {
deleted_ids << row_id
continue
}
kept << row
}
// V requires `unsafe` to fully reassign a `mut` array param.
rows = unsafe { kept }
return GridMutationApplyResult{
deleted_ids: deleted_ids
}
}
// grid_deduplicate_row_ids collects unique non-empty IDs from
// GridRow.id values and raw ID strings. Shared by in-memory
// and ORM delete paths.
fn grid_deduplicate_row_ids(rows []GridRow, row_ids []string) map[string]bool {
mut seen := map[string]bool{}
for row in rows {
if row.id.len > 0 {
seen[row.id] = true
}
}
for row_id in row_ids {
id := row_id.trim_space()
if id.len > 0 {
seen[id] = true
}
}
return seen
}
fn data_grid_source_next_create_row_id(rows []GridRow, existing map[string]bool, preferred_id string) !string {
id := preferred_id.trim_space()
if id.len > 0 && !existing[id] {
return id
}
cap := rows.len + 1000
mut next := rows.len + 1
for next <= cap {
candidate := '${next}'
if !existing[candidate] {
return candidate
}
next++
}
// Numeric range exhausted; try random hex IDs.
for _ in 0 .. 10 {
candidate := '__gen_${rand.u64():016x}'
if !existing[candidate] {
return candidate
}
}
return error('grid: unable to generate unique row id')
}