lqft-python-engine 1.0.6__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/PKG-INFO +1 -1
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_engine.c +431 -12
- lqft_python_engine-1.0.7/lqft_engine.py +284 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/PKG-INFO +1 -1
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/setup.py +2 -2
- lqft_python_engine-1.0.6/lqft_engine.py +0 -146
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/LICENSE.md +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/SOURCES.txt +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/dependency_links.txt +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/requires.txt +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/top_level.txt +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/pyproject.toml +0 -0
- {lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/setup.cfg +0 -0
|
@@ -353,6 +353,24 @@ static ALIGN_64 PaddedRoot global_roots[NUM_ROOTS];
|
|
|
353
353
|
static ALIGN_64 PaddedRWLock root_locks[NUM_ROOTS];
|
|
354
354
|
static ALIGN_64 PaddedLock stripe_locks[NUM_STRIPES];
|
|
355
355
|
|
|
356
|
+
#define VALUE_POOL_BUCKETS 4096
|
|
357
|
+
|
|
358
|
+
typedef struct ValueEntry {
|
|
359
|
+
char* str;
|
|
360
|
+
uint64_t hash;
|
|
361
|
+
volatile long ref_count;
|
|
362
|
+
struct ValueEntry* next;
|
|
363
|
+
} ValueEntry;
|
|
364
|
+
|
|
365
|
+
static ValueEntry* value_pool[VALUE_POOL_BUCKETS] = {0};
|
|
366
|
+
static ALIGN_64 PaddedLock value_pool_locks[VALUE_POOL_BUCKETS];
|
|
367
|
+
static int64_t value_pool_entry_count = 0;
|
|
368
|
+
static int64_t value_pool_total_bytes = 0;
|
|
369
|
+
|
|
370
|
+
static const char* value_acquire(const char* value_ptr);
|
|
371
|
+
static void value_release(const char* value_ptr);
|
|
372
|
+
static void value_pool_clear_all(void);
|
|
373
|
+
|
|
356
374
|
const uint64_t FNV_OFFSET_BASIS = 14695981039346656037ULL;
|
|
357
375
|
const uint64_t FNV_PRIME = 1099511628211ULL;
|
|
358
376
|
|
|
@@ -591,7 +609,7 @@ void decref(LQFTNode* start_node) {
|
|
|
591
609
|
local_ret_arr_count = 0;
|
|
592
610
|
}
|
|
593
611
|
}
|
|
594
|
-
if (node->value)
|
|
612
|
+
if (node->value) value_release((const char*)node->value);
|
|
595
613
|
node->children = (LQFTNode**)local_ret_node_head;
|
|
596
614
|
local_ret_node_head = node;
|
|
597
615
|
if (local_ret_node_count == 0) local_ret_node_tail = node;
|
|
@@ -659,7 +677,9 @@ LQFTNode* get_canonical_v2(const char* value_ptr, uint64_t key_hash, LQFTNode**
|
|
|
659
677
|
}
|
|
660
678
|
fast_unlock(&stripe_locks[stripe].flag);
|
|
661
679
|
|
|
662
|
-
|
|
680
|
+
const char* canonical_value = value_ptr ? value_acquire(value_ptr) : NULL;
|
|
681
|
+
if (value_ptr && !canonical_value) return NULL;
|
|
682
|
+
LQFTNode* new_node = create_node((void*)canonical_value, key_hash, children, full_hash);
|
|
663
683
|
if (!new_node) return NULL;
|
|
664
684
|
new_node->ref_count = 1;
|
|
665
685
|
|
|
@@ -675,7 +695,7 @@ LQFTNode* get_canonical_v2(const char* value_ptr, uint64_t key_hash, LQFTNode**
|
|
|
675
695
|
else if (slot->full_hash_val == full_hash && node_matches_signature(slot, value_ptr, key_hash, children)) {
|
|
676
696
|
ATOMIC_INC(&slot->ref_count);
|
|
677
697
|
fast_unlock(&stripe_locks[stripe].flag);
|
|
678
|
-
if (new_node->value)
|
|
698
|
+
if (new_node->value) value_release((const char*)new_node->value);
|
|
679
699
|
if (new_node->children) {
|
|
680
700
|
LQFTNode*** arr = (LQFTNode***)new_node->children;
|
|
681
701
|
arr[0] = (LQFTNode**)local_ret_arr_head;
|
|
@@ -822,6 +842,33 @@ char* core_search(uint64_t h, LQFTNode* root) {
|
|
|
822
842
|
return NULL;
|
|
823
843
|
}
|
|
824
844
|
|
|
845
|
+
static inline uint64_t hash_key_string(const char* key_str) {
|
|
846
|
+
// One-pass FNV-1a for NUL-terminated UTF-8 keys (avoids strlen + second scan).
|
|
847
|
+
uint64_t h = FNV_OFFSET_BASIS;
|
|
848
|
+
const unsigned char* p = (const unsigned char*)key_str;
|
|
849
|
+
while (*p) {
|
|
850
|
+
h ^= (uint64_t)(*p++);
|
|
851
|
+
h *= FNV_PRIME;
|
|
852
|
+
}
|
|
853
|
+
return h;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
static inline uint64_t fnv1a_update_u64_decimal(uint64_t hash, uint64_t value) {
|
|
857
|
+
// Append unsigned integer digits directly to FNV stream without heap allocation.
|
|
858
|
+
char rev[20];
|
|
859
|
+
int len = 0;
|
|
860
|
+
do {
|
|
861
|
+
rev[len++] = (char)('0' + (value % 10));
|
|
862
|
+
value /= 10;
|
|
863
|
+
} while (value != 0);
|
|
864
|
+
|
|
865
|
+
for (int i = len - 1; i >= 0; i--) {
|
|
866
|
+
hash ^= (uint64_t)(unsigned char)rev[i];
|
|
867
|
+
hash *= FNV_PRIME;
|
|
868
|
+
}
|
|
869
|
+
return hash;
|
|
870
|
+
}
|
|
871
|
+
|
|
825
872
|
static void c_internal_insert_rw(uint64_t h, const char* val_str) {
|
|
826
873
|
// Small TLS cache avoids repeated value hashing for hot constants (e.g. "x", "active").
|
|
827
874
|
static THREAD_LOCAL const char* last_val_ptr = NULL;
|
|
@@ -966,19 +1013,101 @@ static PyObject* method_insert(PyObject* self, PyObject* const* args, Py_ssize_t
|
|
|
966
1013
|
Py_RETURN_NONE;
|
|
967
1014
|
}
|
|
968
1015
|
|
|
1016
|
+
static PyObject* method_insert_key_value(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1017
|
+
if (nargs != 2) return NULL;
|
|
1018
|
+
const char* key_str = PyUnicode_AsUTF8(args[0]);
|
|
1019
|
+
const char* val_str = PyUnicode_AsUTF8(args[1]);
|
|
1020
|
+
if (!key_str || !val_str) return NULL;
|
|
1021
|
+
uint64_t h = hash_key_string(key_str);
|
|
1022
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1023
|
+
c_internal_insert_rw(h, val_str);
|
|
1024
|
+
Py_END_ALLOW_THREADS
|
|
1025
|
+
Py_RETURN_NONE;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
static PyObject* method_bulk_insert_keys(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1029
|
+
if (nargs != 2) return NULL;
|
|
1030
|
+
PyObject* seq = PySequence_Fast(args[0], "bulk_insert_keys expects a sequence of string keys");
|
|
1031
|
+
if (!seq) return NULL;
|
|
1032
|
+
|
|
1033
|
+
const char* val_str = PyUnicode_AsUTF8(args[1]);
|
|
1034
|
+
if (!val_str) {
|
|
1035
|
+
Py_DECREF(seq);
|
|
1036
|
+
return NULL;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
Py_ssize_t n = PySequence_Fast_GET_SIZE(seq);
|
|
1040
|
+
if (n <= 0) {
|
|
1041
|
+
Py_DECREF(seq);
|
|
1042
|
+
Py_RETURN_NONE;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
uint64_t* hashes = (uint64_t*)malloc((size_t)n * sizeof(uint64_t));
|
|
1046
|
+
if (!hashes) {
|
|
1047
|
+
Py_DECREF(seq);
|
|
1048
|
+
return PyErr_NoMemory();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
PyObject** items = PySequence_Fast_ITEMS(seq);
|
|
1052
|
+
for (Py_ssize_t i = 0; i < n; i++) {
|
|
1053
|
+
const char* key_str = PyUnicode_AsUTF8(items[i]);
|
|
1054
|
+
if (!key_str) {
|
|
1055
|
+
free(hashes);
|
|
1056
|
+
Py_DECREF(seq);
|
|
1057
|
+
return NULL;
|
|
1058
|
+
}
|
|
1059
|
+
hashes[i] = hash_key_string(key_str);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1063
|
+
for (Py_ssize_t i = 0; i < n; i++) {
|
|
1064
|
+
c_internal_insert_rw(hashes[i], val_str);
|
|
1065
|
+
}
|
|
1066
|
+
Py_END_ALLOW_THREADS
|
|
1067
|
+
|
|
1068
|
+
free(hashes);
|
|
1069
|
+
Py_DECREF(seq);
|
|
1070
|
+
Py_RETURN_NONE;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
static PyObject* method_bulk_insert_range(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1074
|
+
if (nargs != 4) return NULL;
|
|
1075
|
+
const char* prefix = PyUnicode_AsUTF8(args[0]);
|
|
1076
|
+
if (!prefix) return NULL;
|
|
1077
|
+
unsigned long long start = PyLong_AsUnsignedLongLong(args[1]);
|
|
1078
|
+
if (PyErr_Occurred()) return NULL;
|
|
1079
|
+
unsigned long long count = PyLong_AsUnsignedLongLong(args[2]);
|
|
1080
|
+
if (PyErr_Occurred()) return NULL;
|
|
1081
|
+
const char* val_str = PyUnicode_AsUTF8(args[3]);
|
|
1082
|
+
if (!val_str) return NULL;
|
|
1083
|
+
|
|
1084
|
+
uint64_t prefix_hash = fnv1a_update(FNV_OFFSET_BASIS, prefix, strlen(prefix));
|
|
1085
|
+
|
|
1086
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1087
|
+
for (unsigned long long i = 0; i < count; i++) {
|
|
1088
|
+
uint64_t h = fnv1a_update_u64_decimal(prefix_hash, (uint64_t)(start + i));
|
|
1089
|
+
c_internal_insert_rw(h, val_str);
|
|
1090
|
+
}
|
|
1091
|
+
Py_END_ALLOW_THREADS
|
|
1092
|
+
Py_RETURN_NONE;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
969
1095
|
static PyObject* method_search(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
970
1096
|
if (nargs != 1) return NULL;
|
|
971
1097
|
uint64_t h = PyLong_AsUnsignedLongLongMask(args[0]);
|
|
972
1098
|
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
973
1099
|
char* safe_copy = NULL;
|
|
974
|
-
Py_BEGIN_ALLOW_THREADS
|
|
1100
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1101
|
+
LQFTNode* current_root = NULL;
|
|
975
1102
|
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
976
|
-
|
|
1103
|
+
current_root = global_roots[shard].root;
|
|
1104
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1105
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
977
1106
|
if (current_root) {
|
|
978
|
-
char* result = core_search(h, current_root);
|
|
979
|
-
if (result) safe_copy = portable_strdup(result);
|
|
1107
|
+
char* result = core_search(h, current_root);
|
|
1108
|
+
if (result) safe_copy = portable_strdup(result);
|
|
1109
|
+
decref(current_root);
|
|
980
1110
|
}
|
|
981
|
-
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
982
1111
|
Py_END_ALLOW_THREADS
|
|
983
1112
|
if (safe_copy) {
|
|
984
1113
|
PyObject* py_res = PyUnicode_FromString(safe_copy);
|
|
@@ -987,6 +1116,160 @@ static PyObject* method_search(PyObject* self, PyObject* const* args, Py_ssize_t
|
|
|
987
1116
|
Py_RETURN_NONE;
|
|
988
1117
|
}
|
|
989
1118
|
|
|
1119
|
+
static PyObject* method_search_key(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1120
|
+
if (nargs != 1) return NULL;
|
|
1121
|
+
const char* key_str = PyUnicode_AsUTF8(args[0]);
|
|
1122
|
+
if (!key_str) return NULL;
|
|
1123
|
+
uint64_t h = hash_key_string(key_str);
|
|
1124
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1125
|
+
char* safe_copy = NULL;
|
|
1126
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1127
|
+
LQFTNode* current_root = NULL;
|
|
1128
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1129
|
+
current_root = global_roots[shard].root;
|
|
1130
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1131
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1132
|
+
if (current_root) {
|
|
1133
|
+
char* result = core_search(h, current_root);
|
|
1134
|
+
if (result) safe_copy = portable_strdup(result);
|
|
1135
|
+
decref(current_root);
|
|
1136
|
+
}
|
|
1137
|
+
Py_END_ALLOW_THREADS
|
|
1138
|
+
if (safe_copy) {
|
|
1139
|
+
PyObject* py_res = PyUnicode_FromString(safe_copy);
|
|
1140
|
+
free(safe_copy);
|
|
1141
|
+
return py_res;
|
|
1142
|
+
}
|
|
1143
|
+
Py_RETURN_NONE;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
static PyObject* method_contains(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1147
|
+
if (nargs != 1) return NULL;
|
|
1148
|
+
uint64_t h = PyLong_AsUnsignedLongLongMask(args[0]);
|
|
1149
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1150
|
+
int found = 0;
|
|
1151
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1152
|
+
LQFTNode* current_root = NULL;
|
|
1153
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1154
|
+
current_root = global_roots[shard].root;
|
|
1155
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1156
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1157
|
+
if (current_root) {
|
|
1158
|
+
if (core_search(h, current_root) != NULL) found = 1;
|
|
1159
|
+
decref(current_root);
|
|
1160
|
+
}
|
|
1161
|
+
Py_END_ALLOW_THREADS
|
|
1162
|
+
if (found) Py_RETURN_TRUE;
|
|
1163
|
+
Py_RETURN_FALSE;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
static PyObject* method_contains_key(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1167
|
+
if (nargs != 1) return NULL;
|
|
1168
|
+
const char* key_str = PyUnicode_AsUTF8(args[0]);
|
|
1169
|
+
if (!key_str) return NULL;
|
|
1170
|
+
uint64_t h = hash_key_string(key_str);
|
|
1171
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1172
|
+
int found = 0;
|
|
1173
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1174
|
+
LQFTNode* current_root = NULL;
|
|
1175
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1176
|
+
current_root = global_roots[shard].root;
|
|
1177
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1178
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1179
|
+
if (current_root) {
|
|
1180
|
+
if (core_search(h, current_root) != NULL) found = 1;
|
|
1181
|
+
decref(current_root);
|
|
1182
|
+
}
|
|
1183
|
+
Py_END_ALLOW_THREADS
|
|
1184
|
+
if (found) Py_RETURN_TRUE;
|
|
1185
|
+
Py_RETURN_FALSE;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
static PyObject* method_bulk_contains_count(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1189
|
+
if (nargs != 1) return NULL;
|
|
1190
|
+
PyObject* seq = PySequence_Fast(args[0], "bulk_contains_count expects a sequence of string keys");
|
|
1191
|
+
if (!seq) return NULL;
|
|
1192
|
+
|
|
1193
|
+
Py_ssize_t n = PySequence_Fast_GET_SIZE(seq);
|
|
1194
|
+
if (n <= 0) {
|
|
1195
|
+
Py_DECREF(seq);
|
|
1196
|
+
return PyLong_FromLong(0);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
uint64_t* hashes = (uint64_t*)malloc((size_t)n * sizeof(uint64_t));
|
|
1200
|
+
if (!hashes) {
|
|
1201
|
+
Py_DECREF(seq);
|
|
1202
|
+
return PyErr_NoMemory();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
PyObject** items = PySequence_Fast_ITEMS(seq);
|
|
1206
|
+
for (Py_ssize_t i = 0; i < n; i++) {
|
|
1207
|
+
const char* key_str = PyUnicode_AsUTF8(items[i]);
|
|
1208
|
+
if (!key_str) {
|
|
1209
|
+
free(hashes);
|
|
1210
|
+
Py_DECREF(seq);
|
|
1211
|
+
return NULL;
|
|
1212
|
+
}
|
|
1213
|
+
hashes[i] = hash_key_string(key_str);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
Py_ssize_t hit_count = 0;
|
|
1217
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1218
|
+
for (Py_ssize_t i = 0; i < n; i++) {
|
|
1219
|
+
uint64_t h = hashes[i];
|
|
1220
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1221
|
+
LQFTNode* current_root = NULL;
|
|
1222
|
+
|
|
1223
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1224
|
+
current_root = global_roots[shard].root;
|
|
1225
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1226
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1227
|
+
|
|
1228
|
+
if (current_root) {
|
|
1229
|
+
if (core_search(h, current_root) != NULL) hit_count++;
|
|
1230
|
+
decref(current_root);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
Py_END_ALLOW_THREADS
|
|
1234
|
+
|
|
1235
|
+
free(hashes);
|
|
1236
|
+
Py_DECREF(seq);
|
|
1237
|
+
return PyLong_FromSsize_t(hit_count);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
static PyObject* method_bulk_contains_range_count(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1241
|
+
if (nargs != 3) return NULL;
|
|
1242
|
+
const char* prefix = PyUnicode_AsUTF8(args[0]);
|
|
1243
|
+
if (!prefix) return NULL;
|
|
1244
|
+
unsigned long long start = PyLong_AsUnsignedLongLong(args[1]);
|
|
1245
|
+
if (PyErr_Occurred()) return NULL;
|
|
1246
|
+
unsigned long long count = PyLong_AsUnsignedLongLong(args[2]);
|
|
1247
|
+
if (PyErr_Occurred()) return NULL;
|
|
1248
|
+
|
|
1249
|
+
uint64_t prefix_hash = fnv1a_update(FNV_OFFSET_BASIS, prefix, strlen(prefix));
|
|
1250
|
+
unsigned long long hit_count = 0;
|
|
1251
|
+
|
|
1252
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1253
|
+
for (unsigned long long i = 0; i < count; i++) {
|
|
1254
|
+
uint64_t h = fnv1a_update_u64_decimal(prefix_hash, (uint64_t)(start + i));
|
|
1255
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1256
|
+
LQFTNode* current_root = NULL;
|
|
1257
|
+
|
|
1258
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1259
|
+
current_root = global_roots[shard].root;
|
|
1260
|
+
if (current_root) ATOMIC_INC(¤t_root->ref_count);
|
|
1261
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1262
|
+
|
|
1263
|
+
if (current_root) {
|
|
1264
|
+
if (core_search(h, current_root) != NULL) hit_count++;
|
|
1265
|
+
decref(current_root);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
Py_END_ALLOW_THREADS
|
|
1269
|
+
|
|
1270
|
+
return PyLong_FromUnsignedLongLong(hit_count);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
990
1273
|
static PyObject* method_delete(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
991
1274
|
if (nargs != 1) return NULL;
|
|
992
1275
|
uint64_t h = PyLong_AsUnsignedLongLongMask(args[0]);
|
|
@@ -1015,6 +1298,36 @@ static PyObject* method_delete(PyObject* self, PyObject* const* args, Py_ssize_t
|
|
|
1015
1298
|
Py_RETURN_NONE;
|
|
1016
1299
|
}
|
|
1017
1300
|
|
|
1301
|
+
static PyObject* method_delete_key(PyObject* self, PyObject* const* args, Py_ssize_t nargs) {
|
|
1302
|
+
if (nargs != 1) return NULL;
|
|
1303
|
+
const char* key_str = PyUnicode_AsUTF8(args[0]);
|
|
1304
|
+
if (!key_str) return NULL;
|
|
1305
|
+
uint64_t h = hash_key_string(key_str);
|
|
1306
|
+
uint32_t shard = (uint32_t)((h >> 48) & ROOT_MASK);
|
|
1307
|
+
Py_BEGIN_ALLOW_THREADS
|
|
1308
|
+
while(1) {
|
|
1309
|
+
LQFT_RWLOCK_RDLOCK(&root_locks[shard].lock);
|
|
1310
|
+
LQFTNode* old_root = global_roots[shard].root;
|
|
1311
|
+
if (old_root) ATOMIC_INC(&old_root->ref_count);
|
|
1312
|
+
LQFT_RWLOCK_UNLOCK_RD(&root_locks[shard].lock);
|
|
1313
|
+
LQFTNode* next = core_delete_internal(h, old_root);
|
|
1314
|
+
LQFT_RWLOCK_WRLOCK(&root_locks[shard].lock);
|
|
1315
|
+
if (global_roots[shard].root == old_root) {
|
|
1316
|
+
global_roots[shard].root = next;
|
|
1317
|
+
LQFT_RWLOCK_UNLOCK_WR(&root_locks[shard].lock);
|
|
1318
|
+
if (old_root) { decref(old_root); decref(old_root); }
|
|
1319
|
+
break;
|
|
1320
|
+
} else {
|
|
1321
|
+
LQFT_RWLOCK_UNLOCK_WR(&root_locks[shard].lock);
|
|
1322
|
+
if (next) decref(next);
|
|
1323
|
+
if (old_root) decref(old_root);
|
|
1324
|
+
for(volatile int s = 0; s < 16; s++) { CPU_PAUSE; }
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
Py_END_ALLOW_THREADS
|
|
1328
|
+
Py_RETURN_NONE;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1018
1331
|
static PyObject* method_get_metrics(PyObject* self, PyObject* args) {
|
|
1019
1332
|
int64_t total_phys_added = 0;
|
|
1020
1333
|
int64_t total_phys_freed = 0;
|
|
@@ -1026,19 +1339,30 @@ static PyObject* method_get_metrics(PyObject* self, PyObject* args) {
|
|
|
1026
1339
|
}
|
|
1027
1340
|
int64_t net_phys = total_phys_added - total_phys_freed;
|
|
1028
1341
|
double deduplication_ratio = net_phys > 0 ? (double)total_logical / (double)net_phys : 0.0;
|
|
1029
|
-
|
|
1342
|
+
double value_pool_bytes_per_logical_insert = total_logical > 0
|
|
1343
|
+
? (double)value_pool_total_bytes / (double)total_logical
|
|
1344
|
+
: 0.0;
|
|
1345
|
+
return Py_BuildValue(
|
|
1346
|
+
"{s:L, s:L, s:d, s:L, s:L, s:d}",
|
|
1347
|
+
"physical_nodes", net_phys,
|
|
1348
|
+
"logical_inserts", total_logical,
|
|
1349
|
+
"deduplication_ratio", deduplication_ratio,
|
|
1350
|
+
"value_pool_entries", value_pool_entry_count,
|
|
1351
|
+
"value_pool_bytes", value_pool_total_bytes,
|
|
1352
|
+
"value_pool_bytes_per_logical_insert", value_pool_bytes_per_logical_insert
|
|
1353
|
+
);
|
|
1030
1354
|
}
|
|
1031
1355
|
|
|
1032
1356
|
static PyObject* method_free_all(PyObject* self, PyObject* args) {
|
|
1033
1357
|
Py_BEGIN_ALLOW_THREADS
|
|
1034
1358
|
for(int i = 0; i < NUM_ROOTS; i++) LQFT_RWLOCK_WRLOCK(&root_locks[i].lock);
|
|
1035
1359
|
for(int i = 0; i < NUM_STRIPES; i++) fast_lock_backoff(&stripe_locks[i].flag);
|
|
1036
|
-
if (registry) {
|
|
1360
|
+
if (registry) {
|
|
1037
1361
|
for(int i = 0; i < NUM_STRIPES * STRIPE_SIZE; i++) {
|
|
1038
|
-
|
|
1039
|
-
registry[i] = NULL;
|
|
1362
|
+
registry[i] = NULL;
|
|
1040
1363
|
}
|
|
1041
1364
|
}
|
|
1365
|
+
value_pool_clear_all();
|
|
1042
1366
|
fast_lock_backoff(&global_chunk_lock.flag);
|
|
1043
1367
|
NodeChunk* nc = global_node_chunks;
|
|
1044
1368
|
while(nc) { NodeChunk* n = nc->next_global; free_node_chunk(nc); nc = n; }
|
|
@@ -1082,8 +1406,17 @@ static PyObject* method_free_all(PyObject* self, PyObject* args) {
|
|
|
1082
1406
|
|
|
1083
1407
|
static PyMethodDef LQFTMethods[] = {
|
|
1084
1408
|
{"insert", (PyCFunction)method_insert, METH_FASTCALL, "Fast-path insert single key"},
|
|
1409
|
+
{"insert_key_value", (PyCFunction)method_insert_key_value, METH_FASTCALL, "Insert using string key/value fast path"},
|
|
1410
|
+
{"bulk_insert_keys", (PyCFunction)method_bulk_insert_keys, METH_FASTCALL, "Bulk insert string keys with one value"},
|
|
1411
|
+
{"bulk_insert_range", (PyCFunction)method_bulk_insert_range, METH_FASTCALL, "Bulk insert generated keys prefix+index range"},
|
|
1085
1412
|
{"search", (PyCFunction)method_search, METH_FASTCALL, "Fast-path search single key"},
|
|
1413
|
+
{"search_key", (PyCFunction)method_search_key, METH_FASTCALL, "Search using string key fast path"},
|
|
1414
|
+
{"contains", (PyCFunction)method_contains, METH_FASTCALL, "Contains check by pre-hashed key"},
|
|
1415
|
+
{"contains_key", (PyCFunction)method_contains_key, METH_FASTCALL, "Contains check using string key fast path"},
|
|
1416
|
+
{"bulk_contains_count", (PyCFunction)method_bulk_contains_count, METH_FASTCALL, "Bulk contains checks, returns hit count"},
|
|
1417
|
+
{"bulk_contains_range_count", (PyCFunction)method_bulk_contains_range_count, METH_FASTCALL, "Bulk contains on generated keys prefix+index range"},
|
|
1086
1418
|
{"delete", (PyCFunction)method_delete, METH_FASTCALL, "Fast-path delete single key"},
|
|
1419
|
+
{"delete_key", (PyCFunction)method_delete_key, METH_FASTCALL, "Delete using string key fast path"},
|
|
1087
1420
|
{"internal_stress_test", (PyCFunction)method_internal_stress_test, METH_FASTCALL, "Run native C stress test"},
|
|
1088
1421
|
{"get_metrics", method_get_metrics, METH_VARARGS, "Get stats"},
|
|
1089
1422
|
{"free_all", method_free_all, METH_VARARGS, "Wipe memory"},
|
|
@@ -1099,6 +1432,7 @@ PyMODINIT_FUNC PyInit_lqft_c_engine(void) {
|
|
|
1099
1432
|
}
|
|
1100
1433
|
registry = (LQFTNode**)calloc(NUM_STRIPES * STRIPE_SIZE, sizeof(LQFTNode*));
|
|
1101
1434
|
for(int i = 0; i < NUM_STRIPES; i++) stripe_locks[i].flag = 0;
|
|
1435
|
+
for(int i = 0; i < VALUE_POOL_BUCKETS; i++) value_pool_locks[i].flag = 0;
|
|
1102
1436
|
for(int j = 0; j < 4; j++) {
|
|
1103
1437
|
NodeChunk* nc = alloc_node_chunk();
|
|
1104
1438
|
ChildChunk* cc = alloc_child_chunk();
|
|
@@ -1112,4 +1446,89 @@ PyMODINIT_FUNC PyInit_lqft_c_engine(void) {
|
|
|
1112
1446
|
pthread_t bg_tid; pthread_create(&bg_tid, NULL, background_alloc_thread, NULL); pthread_detach(bg_tid);
|
|
1113
1447
|
#endif
|
|
1114
1448
|
return PyModule_Create(&lqftmodule);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
static const char* value_acquire(const char* value_ptr) {
|
|
1452
|
+
if (!value_ptr) return NULL;
|
|
1453
|
+
|
|
1454
|
+
uint64_t h = fnv1a_update(FNV_OFFSET_BASIS, value_ptr, strlen(value_ptr));
|
|
1455
|
+
uint32_t bucket = (uint32_t)(h & (VALUE_POOL_BUCKETS - 1));
|
|
1456
|
+
|
|
1457
|
+
fast_lock_backoff(&value_pool_locks[bucket].flag);
|
|
1458
|
+
ValueEntry* cur = value_pool[bucket];
|
|
1459
|
+
while (cur) {
|
|
1460
|
+
if (cur->hash == h && strcmp(cur->str, value_ptr) == 0) {
|
|
1461
|
+
ATOMIC_INC(&cur->ref_count);
|
|
1462
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1463
|
+
return cur->str;
|
|
1464
|
+
}
|
|
1465
|
+
cur = cur->next;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
ValueEntry* e = (ValueEntry*)malloc(sizeof(ValueEntry));
|
|
1469
|
+
if (!e) {
|
|
1470
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1471
|
+
return NULL;
|
|
1472
|
+
}
|
|
1473
|
+
size_t value_len = strlen(value_ptr);
|
|
1474
|
+
e->str = portable_strdup(value_ptr);
|
|
1475
|
+
if (!e->str) {
|
|
1476
|
+
free(e);
|
|
1477
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1478
|
+
return NULL;
|
|
1479
|
+
}
|
|
1480
|
+
e->hash = h;
|
|
1481
|
+
e->ref_count = 1;
|
|
1482
|
+
e->next = value_pool[bucket];
|
|
1483
|
+
value_pool[bucket] = e;
|
|
1484
|
+
value_pool_entry_count += 1;
|
|
1485
|
+
value_pool_total_bytes += (int64_t)(value_len + 1);
|
|
1486
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1487
|
+
return e->str;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
static void value_release(const char* value_ptr) {
|
|
1491
|
+
if (!value_ptr) return;
|
|
1492
|
+
|
|
1493
|
+
uint64_t h = fnv1a_update(FNV_OFFSET_BASIS, value_ptr, strlen(value_ptr));
|
|
1494
|
+
uint32_t bucket = (uint32_t)(h & (VALUE_POOL_BUCKETS - 1));
|
|
1495
|
+
|
|
1496
|
+
fast_lock_backoff(&value_pool_locks[bucket].flag);
|
|
1497
|
+
ValueEntry* prev = NULL;
|
|
1498
|
+
ValueEntry* cur = value_pool[bucket];
|
|
1499
|
+
while (cur) {
|
|
1500
|
+
if (cur->hash == h && (cur->str == value_ptr || strcmp(cur->str, value_ptr) == 0)) {
|
|
1501
|
+
long new_ref = ATOMIC_DEC(&cur->ref_count);
|
|
1502
|
+
if (new_ref == 0) {
|
|
1503
|
+
if (prev) prev->next = cur->next;
|
|
1504
|
+
else value_pool[bucket] = cur->next;
|
|
1505
|
+
value_pool_entry_count -= 1;
|
|
1506
|
+
value_pool_total_bytes -= (int64_t)(strlen(cur->str) + 1);
|
|
1507
|
+
free(cur->str);
|
|
1508
|
+
free(cur);
|
|
1509
|
+
}
|
|
1510
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
prev = cur;
|
|
1514
|
+
cur = cur->next;
|
|
1515
|
+
}
|
|
1516
|
+
fast_unlock(&value_pool_locks[bucket].flag);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
static void value_pool_clear_all(void) {
|
|
1520
|
+
for (uint32_t b = 0; b < VALUE_POOL_BUCKETS; b++) {
|
|
1521
|
+
fast_lock_backoff(&value_pool_locks[b].flag);
|
|
1522
|
+
ValueEntry* cur = value_pool[b];
|
|
1523
|
+
while (cur) {
|
|
1524
|
+
ValueEntry* next = cur->next;
|
|
1525
|
+
free(cur->str);
|
|
1526
|
+
free(cur);
|
|
1527
|
+
cur = next;
|
|
1528
|
+
}
|
|
1529
|
+
value_pool[b] = NULL;
|
|
1530
|
+
fast_unlock(&value_pool_locks[b].flag);
|
|
1531
|
+
}
|
|
1532
|
+
value_pool_entry_count = 0;
|
|
1533
|
+
value_pool_total_bytes = 0;
|
|
1115
1534
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import hashlib
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import psutil
|
|
8
|
+
except Exception:
|
|
9
|
+
psutil = None
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------
|
|
12
|
+
# STRICT NATIVE ENTERPRISE WRAPPER (v1.0.5)
|
|
13
|
+
# ---------------------------------------------------------
|
|
14
|
+
# Architect: Parjad Minooei
|
|
15
|
+
# Target: McMaster B.Tech / UofT MScAC Portfolio
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import lqft_c_engine
|
|
19
|
+
except ImportError:
|
|
20
|
+
print("\n[!] CRITICAL FATAL ERROR: Native C-Engine not found.")
|
|
21
|
+
print("[!] The LQFT is now a strictly native database. Pure Python fallback is disabled.")
|
|
22
|
+
print("[!] Run: python setup.py build_ext --inplace\n")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
class LQFT:
|
|
26
|
+
_instance_lock = threading.Lock()
|
|
27
|
+
_live_instances = 0
|
|
28
|
+
__slots__ = (
|
|
29
|
+
"is_native",
|
|
30
|
+
"auto_purge_enabled",
|
|
31
|
+
"max_memory_mb",
|
|
32
|
+
"total_ops",
|
|
33
|
+
"migration_threshold",
|
|
34
|
+
"_process",
|
|
35
|
+
"_closed",
|
|
36
|
+
"_native_insert_kv",
|
|
37
|
+
"_native_search_key",
|
|
38
|
+
"_native_delete_key",
|
|
39
|
+
"_native_contains_key",
|
|
40
|
+
"_native_bulk_insert_keys",
|
|
41
|
+
"_native_bulk_contains_count",
|
|
42
|
+
"_native_bulk_insert_range",
|
|
43
|
+
"_native_bulk_contains_range_count",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# F-03 & F-04: Restored migration_threshold to sync API signatures across the suite
|
|
47
|
+
def __init__(self, migration_threshold=50000):
|
|
48
|
+
self.is_native = True
|
|
49
|
+
# Keep destructive purge opt-in; global C-engine state can be shared by multiple wrappers.
|
|
50
|
+
self.auto_purge_enabled = False
|
|
51
|
+
self.max_memory_mb = 1000.0
|
|
52
|
+
self.total_ops = 0
|
|
53
|
+
self.migration_threshold = migration_threshold
|
|
54
|
+
self._process = psutil.Process(os.getpid()) if psutil else None
|
|
55
|
+
self._closed = False
|
|
56
|
+
self._native_insert_kv = getattr(lqft_c_engine, "insert_key_value", None)
|
|
57
|
+
self._native_search_key = getattr(lqft_c_engine, "search_key", None)
|
|
58
|
+
self._native_delete_key = getattr(lqft_c_engine, "delete_key", None)
|
|
59
|
+
self._native_contains_key = getattr(lqft_c_engine, "contains_key", None)
|
|
60
|
+
self._native_bulk_insert_keys = getattr(lqft_c_engine, "bulk_insert_keys", None)
|
|
61
|
+
self._native_bulk_contains_count = getattr(lqft_c_engine, "bulk_contains_count", None)
|
|
62
|
+
self._native_bulk_insert_range = getattr(lqft_c_engine, "bulk_insert_range", None)
|
|
63
|
+
self._native_bulk_contains_range_count = getattr(lqft_c_engine, "bulk_contains_range_count", None)
|
|
64
|
+
with LQFT._instance_lock:
|
|
65
|
+
LQFT._live_instances += 1
|
|
66
|
+
|
|
67
|
+
def _validate_type(self, key, value=None):
|
|
68
|
+
if not isinstance(key, str):
|
|
69
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
70
|
+
if value is not None and not isinstance(value, str):
|
|
71
|
+
raise TypeError(f"LQFT values must be strings. Received: {type(value).__name__}")
|
|
72
|
+
|
|
73
|
+
def _get_64bit_hash(self, key):
|
|
74
|
+
# Deterministic 64-bit hash keeps key mapping stable across processes/runs.
|
|
75
|
+
return int.from_bytes(hashlib.blake2b(key.encode(), digest_size=8).digest(), "little")
|
|
76
|
+
|
|
77
|
+
def _current_memory_mb(self):
|
|
78
|
+
if self._process is None:
|
|
79
|
+
# Fallback for environments where psutil binary wheels are unavailable.
|
|
80
|
+
if os.name == "nt":
|
|
81
|
+
try:
|
|
82
|
+
import ctypes
|
|
83
|
+
from ctypes import wintypes
|
|
84
|
+
|
|
85
|
+
class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
|
|
86
|
+
_fields_ = [
|
|
87
|
+
("cb", wintypes.DWORD),
|
|
88
|
+
("PageFaultCount", wintypes.DWORD),
|
|
89
|
+
("PeakWorkingSetSize", ctypes.c_size_t),
|
|
90
|
+
("WorkingSetSize", ctypes.c_size_t),
|
|
91
|
+
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
|
|
92
|
+
("QuotaPagedPoolUsage", ctypes.c_size_t),
|
|
93
|
+
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
|
|
94
|
+
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
|
95
|
+
("PagefileUsage", ctypes.c_size_t),
|
|
96
|
+
("PeakPagefileUsage", ctypes.c_size_t),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
counters = PROCESS_MEMORY_COUNTERS()
|
|
100
|
+
counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
|
|
101
|
+
handle = ctypes.windll.kernel32.GetCurrentProcess()
|
|
102
|
+
get_process_memory_info = ctypes.windll.psapi.GetProcessMemoryInfo
|
|
103
|
+
get_process_memory_info.argtypes = [
|
|
104
|
+
wintypes.HANDLE,
|
|
105
|
+
ctypes.POINTER(PROCESS_MEMORY_COUNTERS),
|
|
106
|
+
wintypes.DWORD,
|
|
107
|
+
]
|
|
108
|
+
get_process_memory_info.restype = wintypes.BOOL
|
|
109
|
+
ok = get_process_memory_info(
|
|
110
|
+
handle,
|
|
111
|
+
ctypes.byref(counters),
|
|
112
|
+
counters.cb,
|
|
113
|
+
)
|
|
114
|
+
if ok:
|
|
115
|
+
return counters.WorkingSetSize / (1024 * 1024)
|
|
116
|
+
except Exception:
|
|
117
|
+
return 0.0
|
|
118
|
+
return 0.0
|
|
119
|
+
try:
|
|
120
|
+
return self._process.memory_info().rss / (1024 * 1024)
|
|
121
|
+
except Exception:
|
|
122
|
+
return 0.0
|
|
123
|
+
|
|
124
|
+
def set_auto_purge_threshold(self, threshold: float):
|
|
125
|
+
threshold = float(threshold)
|
|
126
|
+
if threshold <= 0:
|
|
127
|
+
raise ValueError("Auto-purge threshold must be > 0 MB.")
|
|
128
|
+
self.max_memory_mb = threshold
|
|
129
|
+
self.auto_purge_enabled = True
|
|
130
|
+
|
|
131
|
+
def disable_auto_purge(self):
|
|
132
|
+
self.auto_purge_enabled = False
|
|
133
|
+
|
|
134
|
+
def purge(self):
|
|
135
|
+
current_mb = self._current_memory_mb()
|
|
136
|
+
with LQFT._instance_lock:
|
|
137
|
+
live = LQFT._live_instances
|
|
138
|
+
if live > 1:
|
|
139
|
+
print(
|
|
140
|
+
f"\n[WARN CIRCUIT Breaker] Memory {current_mb:.1f} MB but purge skipped "
|
|
141
|
+
f"because {live} LQFT instances are active (shared global engine state)."
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
print(f"\n[WARN CIRCUIT Breaker] Engine exceeded limit (Currently {current_mb:.1f} MB). Auto-Purging!")
|
|
145
|
+
self.clear()
|
|
146
|
+
|
|
147
|
+
def get_stats(self):
|
|
148
|
+
return lqft_c_engine.get_metrics()
|
|
149
|
+
|
|
150
|
+
# F-02: Standardized Metric Mapping (Dunder Method)
|
|
151
|
+
def __len__(self):
|
|
152
|
+
"""Allows native Python len() to fetch logical_inserts from the C-Engine."""
|
|
153
|
+
stats = self.get_stats()
|
|
154
|
+
# Maps directly to the sharded hardware counters in the C-kernel
|
|
155
|
+
return stats.get('logical_inserts', 0)
|
|
156
|
+
|
|
157
|
+
def clear(self):
|
|
158
|
+
# Global clear (shared native state). Keep explicit to avoid accidental data loss.
|
|
159
|
+
return lqft_c_engine.free_all()
|
|
160
|
+
|
|
161
|
+
def insert(self, key, value):
|
|
162
|
+
if type(key) is not str:
|
|
163
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
164
|
+
if type(value) is not str:
|
|
165
|
+
raise TypeError(f"LQFT values must be strings. Received: {type(value).__name__}")
|
|
166
|
+
|
|
167
|
+
# Heuristic Circuit Breaker check
|
|
168
|
+
if self.auto_purge_enabled:
|
|
169
|
+
self.total_ops += 1
|
|
170
|
+
if self.total_ops % 5000 == 0:
|
|
171
|
+
current_mb = self._current_memory_mb()
|
|
172
|
+
if current_mb >= self.max_memory_mb:
|
|
173
|
+
self.purge()
|
|
174
|
+
|
|
175
|
+
if self._native_insert_kv is not None:
|
|
176
|
+
self._native_insert_kv(key, value)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
h = self._get_64bit_hash(key)
|
|
180
|
+
lqft_c_engine.insert(h, value)
|
|
181
|
+
|
|
182
|
+
def search(self, key):
|
|
183
|
+
if type(key) is not str:
|
|
184
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
185
|
+
if self._native_search_key is not None:
|
|
186
|
+
return self._native_search_key(key)
|
|
187
|
+
|
|
188
|
+
h = self._get_64bit_hash(key)
|
|
189
|
+
return lqft_c_engine.search(h)
|
|
190
|
+
|
|
191
|
+
def remove(self, key):
|
|
192
|
+
if type(key) is not str:
|
|
193
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
194
|
+
if self._native_delete_key is not None:
|
|
195
|
+
self._native_delete_key(key)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
h = self._get_64bit_hash(key)
|
|
199
|
+
if hasattr(lqft_c_engine, 'delete'):
|
|
200
|
+
lqft_c_engine.delete(h)
|
|
201
|
+
|
|
202
|
+
def delete(self, key):
|
|
203
|
+
self.remove(key)
|
|
204
|
+
|
|
205
|
+
def contains(self, key):
|
|
206
|
+
if type(key) is not str:
|
|
207
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
208
|
+
if self._native_contains_key is not None:
|
|
209
|
+
return bool(self._native_contains_key(key))
|
|
210
|
+
return self.search(key) is not None
|
|
211
|
+
|
|
212
|
+
def bulk_insert(self, keys, value):
|
|
213
|
+
if type(value) is not str:
|
|
214
|
+
raise TypeError(f"LQFT values must be strings. Received: {type(value).__name__}")
|
|
215
|
+
if self._native_bulk_insert_keys is not None:
|
|
216
|
+
self._native_bulk_insert_keys(keys, value)
|
|
217
|
+
return
|
|
218
|
+
for key in keys:
|
|
219
|
+
self.insert(key, value)
|
|
220
|
+
|
|
221
|
+
def bulk_contains_count(self, keys):
|
|
222
|
+
if self._native_bulk_contains_count is not None:
|
|
223
|
+
return int(self._native_bulk_contains_count(keys))
|
|
224
|
+
count = 0
|
|
225
|
+
for key in keys:
|
|
226
|
+
if self.contains(key):
|
|
227
|
+
count += 1
|
|
228
|
+
return count
|
|
229
|
+
|
|
230
|
+
def bulk_insert_range(self, prefix, start, count, value):
|
|
231
|
+
if type(prefix) is not str:
|
|
232
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(prefix).__name__}")
|
|
233
|
+
if type(value) is not str:
|
|
234
|
+
raise TypeError(f"LQFT values must be strings. Received: {type(value).__name__}")
|
|
235
|
+
if self._native_bulk_insert_range is not None:
|
|
236
|
+
self._native_bulk_insert_range(prefix, int(start), int(count), value)
|
|
237
|
+
return
|
|
238
|
+
end = int(start) + int(count)
|
|
239
|
+
for i in range(int(start), end):
|
|
240
|
+
self.insert(f"{prefix}{i}", value)
|
|
241
|
+
|
|
242
|
+
def bulk_contains_range_count(self, prefix, start, count):
|
|
243
|
+
if type(prefix) is not str:
|
|
244
|
+
raise TypeError(f"LQFT keys must be strings. Received: {type(prefix).__name__}")
|
|
245
|
+
if self._native_bulk_contains_range_count is not None:
|
|
246
|
+
return int(self._native_bulk_contains_range_count(prefix, int(start), int(count)))
|
|
247
|
+
hit = 0
|
|
248
|
+
end = int(start) + int(count)
|
|
249
|
+
for i in range(int(start), end):
|
|
250
|
+
if self.contains(f"{prefix}{i}"):
|
|
251
|
+
hit += 1
|
|
252
|
+
return hit
|
|
253
|
+
|
|
254
|
+
def __setitem__(self, key, value):
|
|
255
|
+
self.insert(key, value)
|
|
256
|
+
|
|
257
|
+
def __getitem__(self, key):
|
|
258
|
+
res = self.search(key)
|
|
259
|
+
if res is None:
|
|
260
|
+
raise KeyError(key)
|
|
261
|
+
return res
|
|
262
|
+
|
|
263
|
+
def __delitem__(self, key):
|
|
264
|
+
self.delete(key)
|
|
265
|
+
|
|
266
|
+
def __del__(self):
|
|
267
|
+
try:
|
|
268
|
+
if not self._closed:
|
|
269
|
+
with LQFT._instance_lock:
|
|
270
|
+
LQFT._live_instances = max(0, LQFT._live_instances - 1)
|
|
271
|
+
self._closed = True
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
def status(self):
|
|
276
|
+
return {
|
|
277
|
+
"mode": "Strict Native C-Engine (Arena Allocator)",
|
|
278
|
+
"items": lqft_c_engine.get_metrics().get('physical_nodes', 0),
|
|
279
|
+
"threshold": f"{self.max_memory_mb} MB Circuit Breaker",
|
|
280
|
+
"auto_purge_enabled": self.auto_purge_enabled,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Retain AdaptiveLQFT alias to support legacy benchmark scripts gracefully
|
|
284
|
+
AdaptiveLQFT = LQFT
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
# ---------------------------------------------------------
|
|
6
|
-
# LQFT BUILD SYSTEM - V1.0.
|
|
6
|
+
# LQFT BUILD SYSTEM - V1.0.7 (Apple Silicon M3 Hotfix)
|
|
7
7
|
# Architect: Parjad Minooei
|
|
8
8
|
# ---------------------------------------------------------
|
|
9
9
|
|
|
@@ -40,7 +40,7 @@ lqft_extension = Extension(
|
|
|
40
40
|
|
|
41
41
|
setup(
|
|
42
42
|
name="lqft-python-engine",
|
|
43
|
-
version="1.0.
|
|
43
|
+
version="1.0.7",
|
|
44
44
|
description="LQFT Engine: 14.3M Ops/sec O(1) Time | O(Σ) Space Data Structure",
|
|
45
45
|
long_description=long_description,
|
|
46
46
|
long_description_content_type="text/markdown",
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import hashlib
|
|
4
|
-
import threading
|
|
5
|
-
|
|
6
|
-
try:
|
|
7
|
-
import psutil
|
|
8
|
-
except Exception:
|
|
9
|
-
psutil = None
|
|
10
|
-
|
|
11
|
-
# ---------------------------------------------------------
|
|
12
|
-
# STRICT NATIVE ENTERPRISE WRAPPER (v1.0.5)
|
|
13
|
-
# ---------------------------------------------------------
|
|
14
|
-
# Architect: Parjad Minooei
|
|
15
|
-
# Target: McMaster B.Tech / UofT MScAC Portfolio
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
import lqft_c_engine
|
|
19
|
-
except ImportError:
|
|
20
|
-
print("\n[!] CRITICAL FATAL ERROR: Native C-Engine not found.")
|
|
21
|
-
print("[!] The LQFT is now a strictly native database. Pure Python fallback is disabled.")
|
|
22
|
-
print("[!] Run: python setup.py build_ext --inplace\n")
|
|
23
|
-
sys.exit(1)
|
|
24
|
-
|
|
25
|
-
class LQFT:
|
|
26
|
-
_instance_lock = threading.Lock()
|
|
27
|
-
_live_instances = 0
|
|
28
|
-
|
|
29
|
-
# F-03 & F-04: Restored migration_threshold to sync API signatures across the suite
|
|
30
|
-
def __init__(self, migration_threshold=50000):
|
|
31
|
-
self.is_native = True
|
|
32
|
-
# Keep destructive purge opt-in; global C-engine state can be shared by multiple wrappers.
|
|
33
|
-
self.auto_purge_enabled = False
|
|
34
|
-
self.max_memory_mb = 1000.0
|
|
35
|
-
self.total_ops = 0
|
|
36
|
-
self.migration_threshold = migration_threshold
|
|
37
|
-
self._process = psutil.Process(os.getpid()) if psutil else None
|
|
38
|
-
self._closed = False
|
|
39
|
-
with LQFT._instance_lock:
|
|
40
|
-
LQFT._live_instances += 1
|
|
41
|
-
|
|
42
|
-
def _validate_type(self, key, value=None):
|
|
43
|
-
if not isinstance(key, str):
|
|
44
|
-
raise TypeError(f"LQFT keys must be strings. Received: {type(key).__name__}")
|
|
45
|
-
if value is not None and not isinstance(value, str):
|
|
46
|
-
raise TypeError(f"LQFT values must be strings. Received: {type(value).__name__}")
|
|
47
|
-
|
|
48
|
-
def _get_64bit_hash(self, key):
|
|
49
|
-
# Deterministic 64-bit hash keeps key mapping stable across processes/runs.
|
|
50
|
-
return int.from_bytes(hashlib.blake2b(key.encode(), digest_size=8).digest(), "little")
|
|
51
|
-
|
|
52
|
-
def _current_memory_mb(self):
|
|
53
|
-
if self._process is None:
|
|
54
|
-
return 0.0
|
|
55
|
-
try:
|
|
56
|
-
return self._process.memory_info().rss / (1024 * 1024)
|
|
57
|
-
except Exception:
|
|
58
|
-
return 0.0
|
|
59
|
-
|
|
60
|
-
def set_auto_purge_threshold(self, threshold: float):
|
|
61
|
-
self.max_memory_mb = threshold
|
|
62
|
-
|
|
63
|
-
def purge(self):
|
|
64
|
-
current_mb = self._current_memory_mb()
|
|
65
|
-
with LQFT._instance_lock:
|
|
66
|
-
live = LQFT._live_instances
|
|
67
|
-
if live > 1:
|
|
68
|
-
print(
|
|
69
|
-
f"\n[⚠️ CIRCUIT Breaker] Memory {current_mb:.1f} MB but purge skipped "
|
|
70
|
-
f"because {live} LQFT instances are active (shared global engine state)."
|
|
71
|
-
)
|
|
72
|
-
return
|
|
73
|
-
print(f"\n[⚠️ CIRCUIT Breaker] Engine exceeded limit (Currently {current_mb:.1f} MB). Auto-Purging!")
|
|
74
|
-
self.clear()
|
|
75
|
-
|
|
76
|
-
def get_stats(self):
|
|
77
|
-
return lqft_c_engine.get_metrics()
|
|
78
|
-
|
|
79
|
-
# F-02: Standardized Metric Mapping (Dunder Method)
|
|
80
|
-
def __len__(self):
|
|
81
|
-
"""Allows native Python len() to fetch logical_inserts from the C-Engine."""
|
|
82
|
-
stats = self.get_stats()
|
|
83
|
-
# Maps directly to the sharded hardware counters in the C-kernel
|
|
84
|
-
return stats.get('logical_inserts', 0)
|
|
85
|
-
|
|
86
|
-
def clear(self):
|
|
87
|
-
# Global clear (shared native state). Keep explicit to avoid accidental data loss.
|
|
88
|
-
return lqft_c_engine.free_all()
|
|
89
|
-
|
|
90
|
-
def insert(self, key, value):
|
|
91
|
-
self._validate_type(key, value)
|
|
92
|
-
self.total_ops += 1
|
|
93
|
-
|
|
94
|
-
# Heuristic Circuit Breaker check
|
|
95
|
-
if self.auto_purge_enabled and self.total_ops % 5000 == 0:
|
|
96
|
-
current_mb = self._current_memory_mb()
|
|
97
|
-
if current_mb >= self.max_memory_mb:
|
|
98
|
-
self.purge()
|
|
99
|
-
|
|
100
|
-
h = self._get_64bit_hash(key)
|
|
101
|
-
lqft_c_engine.insert(h, value)
|
|
102
|
-
|
|
103
|
-
def search(self, key):
|
|
104
|
-
self._validate_type(key)
|
|
105
|
-
h = self._get_64bit_hash(key)
|
|
106
|
-
return lqft_c_engine.search(h)
|
|
107
|
-
|
|
108
|
-
def remove(self, key):
|
|
109
|
-
self._validate_type(key)
|
|
110
|
-
h = self._get_64bit_hash(key)
|
|
111
|
-
if hasattr(lqft_c_engine, 'delete'):
|
|
112
|
-
lqft_c_engine.delete(h)
|
|
113
|
-
|
|
114
|
-
def delete(self, key):
|
|
115
|
-
self.remove(key)
|
|
116
|
-
|
|
117
|
-
def __setitem__(self, key, value):
|
|
118
|
-
self.insert(key, value)
|
|
119
|
-
|
|
120
|
-
def __getitem__(self, key):
|
|
121
|
-
res = self.search(key)
|
|
122
|
-
if res is None:
|
|
123
|
-
raise KeyError(key)
|
|
124
|
-
return res
|
|
125
|
-
|
|
126
|
-
def __delitem__(self, key):
|
|
127
|
-
self.delete(key)
|
|
128
|
-
|
|
129
|
-
def __del__(self):
|
|
130
|
-
try:
|
|
131
|
-
if not self._closed:
|
|
132
|
-
with LQFT._instance_lock:
|
|
133
|
-
LQFT._live_instances = max(0, LQFT._live_instances - 1)
|
|
134
|
-
self._closed = True
|
|
135
|
-
except Exception:
|
|
136
|
-
pass
|
|
137
|
-
|
|
138
|
-
def status(self):
|
|
139
|
-
return {
|
|
140
|
-
"mode": "Strict Native C-Engine (Arena Allocator)",
|
|
141
|
-
"items": lqft_c_engine.get_metrics().get('physical_nodes', 0),
|
|
142
|
-
"threshold": f"{self.max_memory_mb} MB Circuit Breaker"
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# Retain AdaptiveLQFT alias to support legacy benchmark scripts gracefully
|
|
146
|
-
AdaptiveLQFT = LQFT
|
|
File without changes
|
{lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/requires.txt
RENAMED
|
File without changes
|
{lqft_python_engine-1.0.6 → lqft_python_engine-1.0.7}/lqft_python_engine.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|