Kinetica   C#   API  Version 7.2.3.1
RecordKey.cs
Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.Runtime.CompilerServices;
5 using System.Text;
6 
7 namespace kinetica.Records;
8 
16  public sealed class RecordKey
17  {
18  private readonly byte[] _buffer;
19  private int _position;
20  private long _routingHash;
21  private bool _isValid;
22 
26  public RecordKey(int bufferSize)
27  {
28  _buffer = new byte[bufferSize];
29  _position = 0;
30  _routingHash = 0;
31  _isValid = true;
32  }
33 
37  public bool IsValid => _isValid && _position > 0;
38 
42  public long RoutingHash => _routingHash;
43 
47  public long HashCode() => _routingHash;
48 
52  public int Route(IList<int> routingTable)
53  {
54  if (routingTable == null || routingTable.Count == 0)
55  return 0;
56 
57  // Use modulo to find the slot in routing table
58  int slot = (int)(Math.Abs(_routingHash) % routingTable.Count);
59  int rank = routingTable[slot];
60 
61  // Convert 1-based rank to 0-based worker index
62  return rank > 0 ? rank - 1 : 0;
63  }
64 
68  public void Invalidate()
69  {
70  _isValid = false;
71  }
72 
76  public void ComputeHash()
77  {
78  if (_position > 0)
79  {
80  _routingHash = MurmurHash3.Hash128(_buffer, 0, _position);
81  }
82  }
83 
84  #region Add Methods
85 
87  [MethodImpl(MethodImplOptions.AggressiveInlining)]
88  public void AddInt8(sbyte value)
89  {
90  EnsureCapacity(1);
91  _buffer[_position++] = (byte)value;
92  }
93 
95  [MethodImpl(MethodImplOptions.AggressiveInlining)]
96  public void AddInt16(short value)
97  {
98  EnsureCapacity(2);
99  _buffer[_position++] = (byte)value;
100  _buffer[_position++] = (byte)(value >> 8);
101  }
102 
104  [MethodImpl(MethodImplOptions.AggressiveInlining)]
105  public void AddInt(int value)
106  {
107  EnsureCapacity(4);
108  _buffer[_position++] = (byte)value;
109  _buffer[_position++] = (byte)(value >> 8);
110  _buffer[_position++] = (byte)(value >> 16);
111  _buffer[_position++] = (byte)(value >> 24);
112  }
113 
115  [MethodImpl(MethodImplOptions.AggressiveInlining)]
116  public void AddLong(long value)
117  {
118  EnsureCapacity(8);
119  _buffer[_position++] = (byte)value;
120  _buffer[_position++] = (byte)(value >> 8);
121  _buffer[_position++] = (byte)(value >> 16);
122  _buffer[_position++] = (byte)(value >> 24);
123  _buffer[_position++] = (byte)(value >> 32);
124  _buffer[_position++] = (byte)(value >> 40);
125  _buffer[_position++] = (byte)(value >> 48);
126  _buffer[_position++] = (byte)(value >> 56);
127  }
128 
130  [MethodImpl(MethodImplOptions.AggressiveInlining)]
131  public void AddFloat(float value)
132  {
133  var bits = BitConverter.SingleToInt32Bits(value);
134  AddInt(bits);
135  }
136 
138  [MethodImpl(MethodImplOptions.AggressiveInlining)]
139  public void AddDouble(double value)
140  {
141  var bits = BitConverter.DoubleToInt64Bits(value);
142  AddLong(bits);
143  }
144 
146  public void AddString(string value)
147  {
148  if (value == null)
149  {
150  Invalidate();
151  return;
152  }
153 
154  var hash = MurmurHash3.Hash128(Encoding.UTF8.GetBytes(value));
155  AddLong(hash);
156  }
157 
159  public void AddCharN(string value, int length)
160  {
161  if (value == null)
162  {
163  Invalidate();
164  return;
165  }
166 
167  EnsureCapacity(length);
168 
169  var bytes = Encoding.UTF8.GetBytes(value);
170  int copyLen = Math.Min(bytes.Length, length);
171 
172  Array.Copy(bytes, 0, _buffer, _position, copyLen);
173 
174  // Pad with zeros if string is shorter than expected length
175  for (int i = copyLen; i < length; i++)
176  {
177  _buffer[_position + i] = 0;
178  }
179 
180  _position += length;
181  }
182 
184  public void AddDate(string value)
185  {
186  if (value == null)
187  {
188  Invalidate();
189  return;
190  }
191 
192  if (!TryParseDate(value, out int year, out int month, out int day))
193  {
194  // Fall back to string hash
195  AddString(value);
196  return;
197  }
198 
199  // Pack date as integer: year * 10000 + month * 100 + day
200  // Then store as 4-byte hash
201  int packed = year * 10000 + month * 100 + day;
202  AddInt(packed);
203  }
204 
206  public void AddDateTime(string value)
207  {
208  if (value == null)
209  {
210  Invalidate();
211  return;
212  }
213 
214  if (!TryParseDateTime(value, out int year, out int month, out int day,
215  out int hour, out int minute, out int second, out int millis))
216  {
217  // Fall back to string hash
218  AddString(value);
219  return;
220  }
221 
222  // Pack datetime into 8 bytes using bit-packing
223  // Similar to Rust implementation
224  long packed = 0;
225  packed |= ((long)(year - 1900) & 0x3FF) << 54; // 10 bits for year
226  packed |= ((long)month & 0xF) << 50; // 4 bits for month
227  packed |= ((long)day & 0x1F) << 45; // 5 bits for day
228  packed |= ((long)hour & 0x1F) << 40; // 5 bits for hour
229  packed |= ((long)minute & 0x3F) << 34; // 6 bits for minute
230  packed |= ((long)second & 0x3F) << 28; // 6 bits for second
231  packed |= ((long)millis & 0x3FF) << 18; // 10 bits for millis
232 
233  AddLong(packed);
234  }
235 
237  public void AddTime(string value)
238  {
239  if (value == null)
240  {
241  Invalidate();
242  return;
243  }
244 
245  if (!TryParseTime(value, out int hour, out int minute, out int second, out int millis))
246  {
247  // Fall back to string hash
248  AddString(value);
249  return;
250  }
251 
252  // Pack time into 4 bytes
253  int packed = (hour * 3600000) + (minute * 60000) + (second * 1000) + millis;
254  AddInt(packed);
255  }
256 
258  public void AddIpv4(string value)
259  {
260  if (value == null)
261  {
262  Invalidate();
263  return;
264  }
265 
266  if (!TryParseIpv4(value, out uint ip))
267  {
268  // Fall back to string hash
269  AddString(value);
270  return;
271  }
272 
273  AddInt((int)ip);
274  }
275 
277  public void AddDecimal(string value, int precision, int scale)
278  {
279  if (value == null)
280  {
281  Invalidate();
282  return;
283  }
284 
285  // For simplicity, use string hash for decimal
286  // A full implementation would parse and encode the decimal value
287  AddString(value);
288  }
289 
291  public void AddUuid(string value)
292  {
293  if (value == null)
294  {
295  Invalidate();
296  return;
297  }
298 
299  // Hash the UUID string
300  AddString(value);
301  }
302 
303  #endregion
304 
305  #region Parsing Helpers
306 
307  private static bool TryParseDate(string value, out int year, out int month, out int day)
308  {
309  year = month = day = 0;
310 
311  if (value.Length < 10)
312  return false;
313 
314  // YYYY-MM-DD
315  if (int.TryParse(value.AsSpan(0, 4), out year) &&
316  int.TryParse(value.AsSpan(5, 2), out month) &&
317  int.TryParse(value.AsSpan(8, 2), out day))
318  {
319  return true;
320  }
321 
322  return false;
323  }
324 
325  private static bool TryParseDateTime(string value,
326  out int year, out int month, out int day,
327  out int hour, out int minute, out int second, out int millis)
328  {
329  year = month = day = hour = minute = second = millis = 0;
330 
331  if (value.Length < 19)
332  return false;
333 
334  // YYYY-MM-DD HH:MM:SS
335  if (!TryParseDate(value, out year, out month, out day))
336  return false;
337 
338  if (int.TryParse(value.AsSpan(11, 2), out hour) &&
339  int.TryParse(value.AsSpan(14, 2), out minute) &&
340  int.TryParse(value.AsSpan(17, 2), out second))
341  {
342  // Try to parse milliseconds if present
343  if (value.Length > 20 && value[19] == '.')
344  {
345  var msSpan = value.AsSpan(20);
346  int msLen = Math.Min(msSpan.Length, 3);
347  if (int.TryParse(msSpan.Slice(0, msLen), out millis))
348  {
349  // Normalize to 3 digits
350  while (msLen < 3) { millis *= 10; msLen++; }
351  }
352  }
353  return true;
354  }
355 
356  return false;
357  }
358 
359  private static bool TryParseTime(string value,
360  out int hour, out int minute, out int second, out int millis)
361  {
362  hour = minute = second = millis = 0;
363 
364  if (value.Length < 8)
365  return false;
366 
367  // HH:MM:SS
368  if (int.TryParse(value.AsSpan(0, 2), out hour) &&
369  int.TryParse(value.AsSpan(3, 2), out minute) &&
370  int.TryParse(value.AsSpan(6, 2), out second))
371  {
372  // Try to parse milliseconds if present
373  if (value.Length > 9 && value[8] == '.')
374  {
375  var msSpan = value.AsSpan(9);
376  int msLen = Math.Min(msSpan.Length, 3);
377  if (int.TryParse(msSpan.Slice(0, msLen), out millis))
378  {
379  while (msLen < 3) { millis *= 10; msLen++; }
380  }
381  }
382  return true;
383  }
384 
385  return false;
386  }
387 
388  private static bool TryParseIpv4(string value, out uint ip)
389  {
390  ip = 0;
391 
392  var parts = value.Split('.');
393  if (parts.Length != 4)
394  return false;
395 
396  for (int i = 0; i < 4; i++)
397  {
398  if (!byte.TryParse(parts[i], out byte b))
399  return false;
400  ip = (ip << 8) | b;
401  }
402 
403  return true;
404  }
405 
406  #endregion
407 
408  [MethodImpl(MethodImplOptions.AggressiveInlining)]
409  private void EnsureCapacity(int additional)
410  {
411  if (_position + additional > _buffer.Length)
412  {
413  throw new InvalidOperationException(
414  $"RecordKey buffer overflow: need {_position + additional} bytes, have {_buffer.Length}");
415  }
416  }
417  }
418 
422  internal static class MurmurHash3
423  {
424  private const ulong C1 = 0x87c37b91114253d5UL;
425  private const ulong C2 = 0x4cf5ad432745937fUL;
426 
430  public static long Hash128(byte[] data)
431  {
432  return Hash128(data, 0, data.Length);
433  }
434 
438  public static long Hash128(byte[] data, int offset, int length, uint seed = 0)
439  {
440  ulong h1 = seed;
441  ulong h2 = seed;
442 
443  int nblocks = length / 16;
444 
445  // Body
446  for (int i = 0; i < nblocks; i++)
447  {
448  ulong k1 = GetBlock64(data, offset + i * 16);
449  ulong k2 = GetBlock64(data, offset + i * 16 + 8);
450 
451  k1 *= C1;
452  k1 = RotateLeft(k1, 31);
453  k1 *= C2;
454  h1 ^= k1;
455 
456  h1 = RotateLeft(h1, 27);
457  h1 += h2;
458  h1 = h1 * 5 + 0x52dce729;
459 
460  k2 *= C2;
461  k2 = RotateLeft(k2, 33);
462  k2 *= C1;
463  h2 ^= k2;
464 
465  h2 = RotateLeft(h2, 31);
466  h2 += h1;
467  h2 = h2 * 5 + 0x38495ab5;
468  }
469 
470  // Tail
471  int tail = offset + nblocks * 16;
472  int remaining = length - nblocks * 16;
473 
474  ulong k1_tail = 0;
475  ulong k2_tail = 0;
476 
477  switch (remaining)
478  {
479  case 15: k2_tail ^= (ulong)data[tail + 14] << 48; goto case 14;
480  case 14: k2_tail ^= (ulong)data[tail + 13] << 40; goto case 13;
481  case 13: k2_tail ^= (ulong)data[tail + 12] << 32; goto case 12;
482  case 12: k2_tail ^= (ulong)data[tail + 11] << 24; goto case 11;
483  case 11: k2_tail ^= (ulong)data[tail + 10] << 16; goto case 10;
484  case 10: k2_tail ^= (ulong)data[tail + 9] << 8; goto case 9;
485  case 9:
486  k2_tail ^= data[tail + 8];
487  k2_tail *= C2;
488  k2_tail = RotateLeft(k2_tail, 33);
489  k2_tail *= C1;
490  h2 ^= k2_tail;
491  goto case 8;
492  case 8: k1_tail ^= (ulong)data[tail + 7] << 56; goto case 7;
493  case 7: k1_tail ^= (ulong)data[tail + 6] << 48; goto case 6;
494  case 6: k1_tail ^= (ulong)data[tail + 5] << 40; goto case 5;
495  case 5: k1_tail ^= (ulong)data[tail + 4] << 32; goto case 4;
496  case 4: k1_tail ^= (ulong)data[tail + 3] << 24; goto case 3;
497  case 3: k1_tail ^= (ulong)data[tail + 2] << 16; goto case 2;
498  case 2: k1_tail ^= (ulong)data[tail + 1] << 8; goto case 1;
499  case 1:
500  k1_tail ^= data[tail];
501  k1_tail *= C1;
502  k1_tail = RotateLeft(k1_tail, 31);
503  k1_tail *= C2;
504  h1 ^= k1_tail;
505  break;
506  }
507 
508  // Finalization
509  h1 ^= (ulong)length;
510  h2 ^= (ulong)length;
511 
512  h1 += h2;
513  h2 += h1;
514 
515  h1 = FMix64(h1);
516  h2 = FMix64(h2);
517 
518  h1 += h2;
519  // h2 += h1; // Not needed since we only return h1
520 
521  return (long)h1;
522  }
523 
524  [MethodImpl(MethodImplOptions.AggressiveInlining)]
525  private static ulong GetBlock64(byte[] data, int offset)
526  {
527  return BitConverter.ToUInt64(data, offset);
528  }
529 
530  [MethodImpl(MethodImplOptions.AggressiveInlining)]
531  private static ulong RotateLeft(ulong x, int r)
532  {
533  return (x << r) | (x >> (64 - r));
534  }
535 
536  [MethodImpl(MethodImplOptions.AggressiveInlining)]
537  private static ulong FMix64(ulong k)
538  {
539  k ^= k >> 33;
540  k *= 0xff51afd7ed558ccdUL;
541  k ^= k >> 33;
542  k *= 0xc4ceb9fe1a85ec53UL;
543  k ^= k >> 33;
544  return k;
545  }
546  }
static long Hash128(byte[] data)
Computes a 128-bit MurmurHash3 and returns the lower 64 bits.
Definition: RecordKey.cs:430
long RoutingHash
Gets the routing hash (must call ComputeHash first).
Definition: RecordKey.cs:42
void AddUuid(string value)
Adds a UUID string to the key.
Definition: RecordKey.cs:291
static long Hash128(byte[] data, int offset, int length, uint seed=0)
Computes a 128-bit MurmurHash3 and returns the lower 64 bits.
Definition: RecordKey.cs:438
void Invalidate()
Invalidates this key (e.g., when a null value is encountered).
Definition: RecordKey.cs:68
A binary key used for shard routing.
Definition: RecordKey.cs:16
void ComputeHash()
Computes the routing hash from the buffer contents.
Definition: RecordKey.cs:76
void AddInt16(short value)
Adds a 16-bit integer to the key (little-endian).
Definition: RecordKey.cs:96
void AddTime(string value)
Adds a time string (HH:MM:SS.mmm) to the key.
Definition: RecordKey.cs:237
void AddInt8(sbyte value)
Adds an 8-bit integer to the key.
Definition: RecordKey.cs:88
RecordKey(int bufferSize)
Creates a new RecordKey with the specified buffer size.
Definition: RecordKey.cs:26
void AddIpv4(string value)
Adds an IPv4 address string (dotted-quad) to the key.
Definition: RecordKey.cs:258
void AddDecimal(string value, int precision, int scale)
Adds a decimal string to the key.
Definition: RecordKey.cs:277
void AddDateTime(string value)
Adds a datetime string (YYYY-MM-DD HH:MM:SS.mmm) to the key.
Definition: RecordKey.cs:206
long HashCode()
Gets the hash code for stripe distribution.
void AddCharN(string value, int length)
Adds a fixed-length char to the key.
Definition: RecordKey.cs:159
void AddString(string value)
Adds a string to the key (via MurmurHash3).
Definition: RecordKey.cs:146
bool IsValid
Returns true if this key is valid (no null values were added).
Definition: RecordKey.cs:37
void AddLong(long value)
Adds a 64-bit integer to the key (little-endian).
Definition: RecordKey.cs:116
void AddDate(string value)
Adds a date string (YYYY-MM-DD) to the key.
Definition: RecordKey.cs:184
void AddDouble(double value)
Adds a 64-bit double to the key.
Definition: RecordKey.cs:139
void AddInt(int value)
Adds a 32-bit integer to the key (little-endian).
Definition: RecordKey.cs:105
int Route(IList< int > routingTable)
Routes this key to a worker index using the routing table.
Definition: RecordKey.cs:52
void AddFloat(float value)
Adds a 32-bit float to the key.
Definition: RecordKey.cs:131
MurmurHash3 implementation for 128-bit hashing.
Definition: RecordKey.cs:422