sqlmath 2026.8.31__cp315-cp315-win_amd64.whl

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.
sqlmath/__init__.py ADDED
@@ -0,0 +1,859 @@
1
+ # Copyright (c) 2021 Kai Zhu
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+
22
+ """sqlmath.py."""
23
+
24
+ __version__ = "2026.8.31"
25
+ __version_info__ = ("2026", "8", "31")
26
+
27
+ import csv
28
+ import io
29
+ import json
30
+ import math
31
+ import pathlib
32
+ import platform
33
+ import re
34
+ import struct
35
+ import sys
36
+ import weakref
37
+
38
+ from . import _sqlmath
39
+
40
+ DB_STATE = {
41
+ "init": None,
42
+ "lgbm": None,
43
+ }
44
+
45
+
46
+ JSBATON_ARGC = 8
47
+ JSBATON_OFFSET_ALL = 256
48
+ JSBATON_OFFSET_ARGV = 128
49
+ JSBATON_OFFSET_FUNCNAME = 8
50
+ SIZEOF_BLOB_MAX = 1_000_000_000
51
+ SQLITE_DATATYPE_BLOB = 0x04
52
+ SQLITE_DATATYPE_EXTERNALBUFFER = 0x71
53
+ SQLITE_DATATYPE_FLOAT = 0x02
54
+ SQLITE_DATATYPE_INTEGER = 0x01
55
+ SQLITE_DATATYPE_INTEGER_0 = 0x00
56
+ SQLITE_DATATYPE_INTEGER_1 = 0x21
57
+ SQLITE_DATATYPE_NULL = 0x05
58
+ SQLITE_DATATYPE_TEXT = 0x03
59
+ SQLITE_DATATYPE_TEXT_0 = 0x13
60
+ SQLITE_RESPONSETYPE_LASTBLOB = 1
61
+
62
+
63
+ LGBM_DTYPE_FLOAT32 = 0 # float32 (single precision float)
64
+ LGBM_DTYPE_FLOAT64 = 1 # float64 (double precision float)
65
+ LGBM_DTYPE_INT32 = 2 # int32
66
+ LGBM_DTYPE_INT64 = 3 # int64
67
+ LGBM_FEATURE_IMPORTANCE_GAIN = 1 # Gain type of feature importance
68
+ LGBM_FEATURE_IMPORTANCE_SPLIT = 0 # Split type of feature importance
69
+ LGBM_MATRIX_TYPE_CSC = 1 # CSC sparse matrix type
70
+ LGBM_MATRIX_TYPE_CSR = 0 # CSR sparse matrix type
71
+ LGBM_PREDICT_CONTRIB = 3 # Predict feature contributions (SHAP values)
72
+ LGBM_PREDICT_LEAF_INDEX = 2 # Predict leaf index
73
+ LGBM_PREDICT_NORMAL = 0 # Normal prediction w/ transform (if needed)
74
+ LGBM_PREDICT_RAW_SCORE = 1 # Predict raw score
75
+ SQLITE_OPEN_AUTOPROXY = 0x00000020 # VFS only
76
+ SQLITE_OPEN_CREATE = 0x00000004 # Ok for sqlite3_open_v2()
77
+ SQLITE_OPEN_DELETEONCLOSE = 0x00000008 # VFS only
78
+ SQLITE_OPEN_EXCLUSIVE = 0x00000010 # VFS only
79
+ SQLITE_OPEN_FULLMUTEX = 0x00010000 # Ok for sqlite3_open_v2()
80
+ SQLITE_OPEN_MAIN_DB = 0x00000100 # VFS only
81
+ SQLITE_OPEN_MAIN_JOURNAL = 0x00000800 # VFS only
82
+ SQLITE_OPEN_MEMORY = 0x00000080 # Ok for sqlite3_open_v2()
83
+ SQLITE_OPEN_NOFOLLOW = 0x01000000 # Ok for sqlite3_open_v2()
84
+ SQLITE_OPEN_NOMUTEX = 0x00008000 # Ok for sqlite3_open_v2()
85
+ SQLITE_OPEN_PRIVATECACHE = 0x00040000 # Ok for sqlite3_open_v2()
86
+ SQLITE_OPEN_READONLY = 0x00000001 # Ok for sqlite3_open_v2()
87
+ SQLITE_OPEN_READWRITE = 0x00000002 # Ok for sqlite3_open_v2()
88
+ SQLITE_OPEN_SHAREDCACHE = 0x00020000 # Ok for sqlite3_open_v2()
89
+ SQLITE_OPEN_SUBJOURNAL = 0x00002000 # VFS only
90
+ SQLITE_OPEN_SUPER_JOURNAL = 0x00004000 # VFS only
91
+ SQLITE_OPEN_TEMP_DB = 0x00000200 # VFS only
92
+ SQLITE_OPEN_TEMP_JOURNAL = 0x00001000 # VFS only
93
+ SQLITE_OPEN_TRANSIENT_DB = 0x00000400 # VFS only
94
+ SQLITE_OPEN_URI = 0x00000040 # Ok for sqlite3_open_v2()
95
+ SQLITE_OPEN_WAL = 0x00080000 # VFS only
96
+
97
+
98
+ INFINITY = float("inf")
99
+ NAN = float("nan")
100
+
101
+
102
+ class SqlmathDb:
103
+ """Sqlmath database class."""
104
+
105
+ closed = False
106
+ filename = ""
107
+ ptr = 0
108
+
109
+ def __bool__(self):
110
+ """Return True if database is open."""
111
+ return not self.closed
112
+
113
+ def __enter__(self):
114
+ """Enter context manager."""
115
+ return self
116
+
117
+ def __exit__(self, exc_type, exc_val, exc_tb):
118
+ """Exit context manager and close database."""
119
+ db_close(self)
120
+ return False
121
+
122
+ def close(self):
123
+ """Close database connection. Alias for db_close(self)."""
124
+ db_close(self)
125
+
126
+ def __repr__(self):
127
+ """Return string representation for debugging."""
128
+ state = "closed" if self.closed else "open"
129
+ return f"SqlmathDb({self.filename!r}, {state})"
130
+
131
+ def execute(self, sql, bind_list=None, response_type=None):
132
+ """Execute SQL statement. Alias for db_exec(db=self, ...)."""
133
+ return db_exec(
134
+ db=self,
135
+ sql=sql,
136
+ bind_list=bind_list,
137
+ response_type=response_type,
138
+ )
139
+
140
+
141
+ class SqlmathError(Exception):
142
+ """Sqlmath error."""
143
+
144
+
145
+ def assert_error_thrown(func, regexp):
146
+ """This function will assert calling <func> throws an error."""
147
+ err = None
148
+ try:
149
+ func()
150
+ except Exception as err_caught: # noqa: BLE001
151
+ err = err_caught
152
+ assert_or_throw(err, "No error thrown.")
153
+ assert_or_throw(not regexp or re.search(regexp, str(err)), err)
154
+
155
+
156
+ def assert_int64(val):
157
+ """This function will assert <val> is within range."""
158
+ """of c99-signed-long-long."""
159
+ assert_or_throw(
160
+ -9_223_372_036_854_775_808 <= val <= 9_223_372_036_854_775_807, # noqa: PLR2004
161
+ (
162
+ f"integer {val} outside signed-64-bit inclusive-range"
163
+ " -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807"
164
+ ),
165
+ )
166
+
167
+
168
+ def assert_json_equal(aa, bb, message=None):
169
+ """This function will assert."""
170
+ """JSON.stringify(<aa>) === JSON.stringify(<bb>)."""
171
+ aa = json.dumps(objectdeepcopywithkeyssorted(aa), indent=1)
172
+ bb = json.dumps(objectdeepcopywithkeyssorted(bb), indent=1)
173
+ if aa != bb:
174
+ raise SqlmathError(
175
+ f"\n{aa}\n!==\n{bb}"
176
+ + (
177
+ " - " + message
178
+ if isinstance(message, str)
179
+ else " - " + json.dumps(message)
180
+ if message
181
+ else ""
182
+ ),
183
+ )
184
+
185
+
186
+ def assert_or_throw(condition, message):
187
+ """This function will throw <message> if <condition> is falsy."""
188
+ if not condition:
189
+ raise (
190
+ SqlmathError(str(message)[:2048])
191
+ if not message or isinstance(message, str)
192
+ else message
193
+ )
194
+
195
+
196
+ def csv_to_list_of_list(csv_text):
197
+ """This function will convert <csv>-text to json list-of-list."""
198
+ # Normalize line endings and trim as per original logic
199
+ csv_text = csv_text.rstrip() + "\n"
200
+ # Use io.StringIO to treat the string as a file for the csv reader
201
+ file = io.StringIO(csv_text)
202
+ reader = csv.reader(
203
+ file,
204
+ quotechar='"',
205
+ delimiter=",",
206
+ quoting=csv.QUOTE_MINIMAL,
207
+ skipinitialspace=False,
208
+ )
209
+ return [row for row in reader if row]
210
+
211
+
212
+ def db_call(baton, arglist):
213
+ """This function will call c-function dbXxx() with given <funcname>."""
214
+ """and return [<baton>, ...arglist]."""
215
+ assert_or_throw(
216
+ len(arglist) <= JSBATON_ARGC,
217
+ f"db_call - len(arglist) must be less than than {JSBATON_ARGC}",
218
+ )
219
+ # serialize js-value to c-value
220
+ for argi, val in enumerate(arglist):
221
+ if val is None:
222
+ # ctype-q = long-long
223
+ struct.pack_into("q", baton, JSBATON_OFFSET_ARGV + argi * 8, 0)
224
+ continue
225
+ if isinstance(val, (bool, float, int)):
226
+ val2 = val
227
+ if isinstance(val, float):
228
+ assert_or_throw(
229
+ int(val) == val,
230
+ f"db_call - float {val} is not an integer",
231
+ )
232
+ val2 = int(val)
233
+ assert_int64(val2)
234
+ # ctype-q = long-long
235
+ struct.pack_into("q", baton, JSBATON_OFFSET_ARGV + argi * 8, val2)
236
+ continue
237
+ if isinstance(val, str):
238
+ baton = jsbaton_set_value(
239
+ baton,
240
+ argi,
241
+ val if val.endswith("\u0000") else val + "\u0000",
242
+ None,
243
+ None,
244
+ )
245
+ continue
246
+ msg = f'db_call - invalid arg-type "{type(val)}"'
247
+ raise SqlmathError(msg)
248
+ _sqlmath.pydbCall(baton)
249
+ return [baton]
250
+
251
+
252
+ def db_close(db):
253
+ """This function will close sqlite-database-connection <db>."""
254
+ # prevent segfault - do not close db if actions are pending
255
+ if not db.closed:
256
+ db.closed = True
257
+ db_call(jsbaton_create("_dbClose"), [db.ptr, db.filename])
258
+
259
+
260
+ def db_exec(
261
+ bind_list=None,
262
+ db=None,
263
+ response_type=None,
264
+ sql=None,
265
+ ):
266
+ """This function will exec <sql> in <db> and return <result>."""
267
+ if bind_list is None:
268
+ bind_list = []
269
+ baton = jsbaton_create("_dbExec")
270
+ bind_by_key = callable(getattr(bind_list, "items", None))
271
+ bufi = [0]
272
+ externalbuffer_list = []
273
+ if bind_by_key:
274
+ for key, val in bind_list.items():
275
+ baton = jsbaton_set_value(baton, None, f":{key}\u0000", None, None)
276
+ baton = jsbaton_set_value(
277
+ baton,
278
+ None,
279
+ val,
280
+ bufi,
281
+ externalbuffer_list,
282
+ )
283
+ else:
284
+ for val in bind_list:
285
+ baton = jsbaton_set_value(
286
+ baton,
287
+ None,
288
+ val,
289
+ bufi,
290
+ externalbuffer_list,
291
+ )
292
+ # bugfix - Re-assign baton, in-case it was resized.
293
+ [baton] = db_call(
294
+ baton,
295
+ [
296
+ # 0. db
297
+ db.ptr,
298
+ # 1. sql
299
+ str(sql) + "\n;\nPRAGMA noop",
300
+ # 2. len(bind_list)
301
+ len(bind_list),
302
+ # 3. bind_by_key
303
+ bind_by_key,
304
+ # 4. response_type
305
+ (
306
+ SQLITE_RESPONSETYPE_LASTBLOB
307
+ if response_type == "lastblob"
308
+ else 0
309
+ ),
310
+ ],
311
+ )
312
+ match response_type:
313
+ case "arraybuffer":
314
+ return memoryview(_sqlmath.pybatonStealCbuffer(baton, 0, 0))
315
+ case "lastblob":
316
+ return memoryview(_sqlmath.pybatonStealCbuffer(baton, 0, 0))
317
+ case "list":
318
+ return json.loads(_sqlmath.pybatonStealCbuffer(baton, 0, 1))
319
+ case _:
320
+ table_list = []
321
+ json_raw = _sqlmath.pybatonStealCbuffer(baton, 0, 1)
322
+ for table in json.loads(json_raw):
323
+ col_list = tuple(enumerate(table.pop(0)))
324
+ table_list.append([
325
+ {key: row[ii] for ii, key in col_list}
326
+ for row in table
327
+ ])
328
+ return table_list
329
+
330
+
331
+ def db_exec_and_return_lastblob(
332
+ bind_list=None,
333
+ db=None,
334
+ sql=None,
335
+ ):
336
+ """This function will exec <sql> in <db> and return last value."""
337
+ """ retrieved. from execution as raw blob/buffer."""
338
+ return db_exec(
339
+ bind_list=bind_list,
340
+ db=db,
341
+ response_type="lastblob",
342
+ sql=sql,
343
+ )
344
+
345
+
346
+ def db_file_load(
347
+ db=None,
348
+ filename=None,
349
+ mode_save=0,
350
+ ):
351
+ """This function will load <filename> to <db>."""
352
+ assert_or_throw(
353
+ isinstance(filename, str) and filename,
354
+ f"invalid filename {filename}",
355
+ )
356
+ db_call(
357
+ jsbaton_create("_dbFileLoad"),
358
+ [
359
+ # 0. sqlite3 * pInMemory
360
+ db.ptr,
361
+ # 1. char *zFilename
362
+ filename,
363
+ # 2. const int isSave
364
+ mode_save,
365
+ ],
366
+ )
367
+
368
+
369
+ def db_file_save(
370
+ db=None,
371
+ filename=None,
372
+ ):
373
+ """This function will save <db> to <filename>."""
374
+ return db_file_load(
375
+ db=db,
376
+ filename=filename,
377
+ mode_save=1,
378
+ )
379
+
380
+
381
+ def db_noop(*arglist):
382
+ """This function will do nothing except return <arglist>."""
383
+ return db_call(jsbaton_create("_dbNoop"), arglist)
384
+
385
+
386
+ def db_open(
387
+ filename=":memory:",
388
+ flags=None,
389
+ timeout_busy=5000,
390
+ ):
391
+ """
392
+ This function will open and return sqlite-database-connection <db>.
393
+
394
+ int sqlite3_open_v2(
395
+ const char *filename, /* Database filename (UTF-8) */
396
+ sqlite3 **ppDb, /* OUT: SQLite db handle */
397
+ int flags, /* Flags */
398
+ const char *zVfs /* Name of VFS module to use */
399
+ )
400
+ """
401
+ assert_or_throw(isinstance(filename, str), f"invalid filename {filename}")
402
+ ptr = db_call(
403
+ jsbaton_create("_dbOpen"),
404
+ [
405
+ # 0. const char *filename, Database filename (UTF-8)
406
+ filename,
407
+ # 1. sqlite3 **ppDb, OUT: SQLite db handle
408
+ None,
409
+ # 2. int flags, Flags
410
+ (
411
+ SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_URI
412
+ if flags is None
413
+ else flags
414
+ ),
415
+ # 3. const char *zVfs Name of VFS module to use
416
+ None,
417
+ ],
418
+ )[0]
419
+ # ctype-q = long-long
420
+ ptr = struct.unpack_from("q", ptr, JSBATON_OFFSET_ARGV + 0)[0]
421
+ db = SqlmathDb()
422
+ db.filename = filename
423
+ db.ptr = ptr
424
+ weakref.finalize(db, db_close, db)
425
+ if not DB_STATE["init"]:
426
+ DB_STATE["init"] = True
427
+ # PRAGMA busy_timeout
428
+ db_exec(
429
+ db=db,
430
+ sql=f"""
431
+ PRAGMA busy_timeout = {timeout_busy};
432
+ """,
433
+ )
434
+ # LGBM_DLOPEN
435
+ lib_lgbm = f"lib_lightgbm_{lib_platform_arch_ext()}"
436
+ lib_lgbm = pathlib.Path(__file__).resolve().parent / lib_lgbm
437
+ if lib_lgbm.exists():
438
+ db_exec(
439
+ db=db,
440
+ sql=f"""
441
+ SELECT LGBM_DLOPEN('{lib_lgbm}');
442
+ """,
443
+ )
444
+ DB_STATE["lgbm"] = True
445
+ return db
446
+
447
+
448
+ def db_table_import( # noqa: PLR0917
449
+ db=None,
450
+ filename=None,
451
+ header_missing=None,
452
+ mode=None,
453
+ table_name=None,
454
+ text_data=None,
455
+ ):
456
+ """This function will create table from imported csv/json <textData>."""
457
+ row_list = []
458
+ rowid_list = []
459
+ if filename:
460
+ with pathlib.Path(filename).open(encoding="utf-8") as f:
461
+ text_data = f.read()
462
+ if text_data is None:
463
+ text_data = ""
464
+ if mode == "csv":
465
+ row_list = csv_to_list_of_list(text_data)
466
+ elif mode == "tsv":
467
+ row_list = [
468
+ line.split("\t") for line in text_data.strip().splitlines()
469
+ ]
470
+ # elif mode == "json":
471
+ else:
472
+ try:
473
+ row_list = json.loads(text_data)
474
+ except json.JSONDecodeError:
475
+ row_list = []
476
+ # Normalize row_list if it's a dictionary
477
+ if isinstance(row_list, dict):
478
+ rowid_list = list(row_list.keys())
479
+ row_list = list(row_list.values())
480
+ if not isinstance(row_list, list):
481
+ row_list = []
482
+ # Handle header_missing
483
+ if header_missing and row_list and isinstance(row_list[0], list):
484
+ header = [str(i + 1) for i in range(len(row_list[0]))]
485
+ row_list.insert(0, header)
486
+ # Normalize empty data
487
+ if not row_list:
488
+ row_list = [["undefined"]]
489
+ # Normalize objects to lists if necessary
490
+ if not isinstance(row_list[0], list):
491
+ # Extract unique keys across all objects
492
+ all_keys = []
493
+ for obj in row_list:
494
+ if isinstance(obj, dict):
495
+ all_keys.extend(obj.keys())
496
+ # Preserve order and get unique keys
497
+ col_list = list(dict.fromkeys(all_keys))
498
+ normalized_rows = [col_list] # First row is headers
499
+ for obj in row_list:
500
+ normalized_rows.extend(
501
+ [obj.get(key) for key in col_list]
502
+ for obj in row_list
503
+ )
504
+ row_list = normalized_rows
505
+ # Extract col_list from first row
506
+ col_list = row_list.pop(0)
507
+ # Preserve rowid if it existed from a dict import
508
+ if rowid_list:
509
+ col_list.insert(0, "rowid")
510
+ for i in range(len(row_list)):
511
+ row_list[i].insert(0, rowid_list[i])
512
+ # Normalize column names (regex sanitization)
513
+ seen_cols = set()
514
+ final_cols = []
515
+ for col_name in col_list:
516
+ clean_name = str(col_name).strip()
517
+ clean_name = re.sub(r"\W", "_", clean_name)
518
+ # Ensure it starts with a letter or underscore
519
+ if not re.match(r"[A-Z_a-z]", clean_name) or clean_name == "":
520
+ clean_name = "_" + clean_name
521
+ # Handle duplicates
522
+ suffix = 1
523
+ candidate = clean_name
524
+ while candidate in seen_cols:
525
+ suffix += 1
526
+ candidate = f"{clean_name}_{suffix}"
527
+ seen_cols.add(candidate)
528
+ final_cols.append(candidate)
529
+ # create dbtable from rowList
530
+ db_exec(
531
+ bind_list={
532
+ "row_list": json.dumps(row_list),
533
+ },
534
+ db=db,
535
+ sql=(
536
+ f"CREATE TABLE {table_name} ({','.join(final_cols)});"
537
+ if not row_list else
538
+ (
539
+ f"CREATE TABLE {table_name} AS SELECT "
540
+ + ",".join(
541
+ [
542
+ f"value->>{ii} AS {col}"
543
+ for ii, col in enumerate(final_cols)
544
+ ],
545
+ )
546
+ + " FROM JSON_EACH($row_list);"
547
+ )
548
+ ),
549
+ )
550
+
551
+
552
+ def debuginline(*argv):
553
+ """This function will print <argv> to stderr and then return <argv>[0]."""
554
+ arg0 = argv[0] if argv else None
555
+ print("\n\ndebuginline", file=sys.stderr)
556
+ print(*argv, file=sys.stderr)
557
+ print("\n", file=sys.stderr)
558
+ return arg0
559
+
560
+
561
+ def jsbaton_create(funcname):
562
+ """This function will create buffer <baton>."""
563
+ baton = memoryview(_sqlmath.PyBaton(1024))
564
+ # init nalloc, nused
565
+ struct.pack_into("i", baton, 4, JSBATON_OFFSET_ALL) # ctype-i = int
566
+ # copy funcname into baton
567
+ funcname = bytes(funcname, "utf-8")
568
+ baton[
569
+ JSBATON_OFFSET_FUNCNAME: JSBATON_OFFSET_FUNCNAME + len(funcname)
570
+ ] = funcname
571
+ return baton
572
+
573
+
574
+ def jsbaton_get_int64(baton, argi):
575
+ """This function will return int64-value from <baton> at <argi>."""
576
+ # ctype-q = long-long
577
+ return struct.unpack_from("q", baton, JSBATON_OFFSET_ARGV + argi * 8)[0]
578
+
579
+
580
+ def jsbaton_get_string(baton, argi):
581
+ """This function will return string-value from <baton> at <argi>."""
582
+ # ctype-q = long-long
583
+ offset = struct.unpack_from("q", baton, JSBATON_OFFSET_ARGV + argi * 8)[0]
584
+ return str(
585
+ baton[
586
+ offset + 1 + 4:
587
+ # remove null-terminator from string
588
+ # ctype-i = int
589
+ offset + 1 + 4 + struct.unpack_from("i", baton, offset + 1)[0] - 1
590
+ ],
591
+ "utf-8",
592
+ )
593
+
594
+
595
+ def jsbaton_set_value(baton, argi, val, bufi, reference_list):
596
+ """
597
+ This function will push <val> to buffer <baton>.
598
+
599
+ #define SQLITE_DATATYPE_BLOB 0x04
600
+ #define SQLITE_DATATYPE_EXTERNALBUFFER 0x71
601
+ #define SQLITE_DATATYPE_FLOAT 0x02
602
+ #define SQLITE_DATATYPE_INTEGER 0x01
603
+ #define SQLITE_DATATYPE_INTEGER_0 0x00
604
+ #define SQLITE_DATATYPE_INTEGER_1 0x21
605
+ #define SQLITE_DATATYPE_NULL 0x05
606
+ #define SQLITE_DATATYPE_TEXT 0x03
607
+ #define SQLITE_DATATYPE_TEXT_0 0x13
608
+
609
+ # 1. 0.NoneType
610
+ # 2. 0.bool
611
+ # 3. 0.bytearray
612
+ # 4. 0.bytes
613
+ # 5. 0.complex
614
+ # 6. 0.dict
615
+ # 7. 0.float
616
+ # 8. 0.frozenset
617
+ # 9. 0.int
618
+ # 10. 0.list
619
+ # 11. 0.memoryview
620
+ # 12. 0.range
621
+ # 13. 0.set
622
+ # 14. 0.str
623
+ # 15. 0.tuple
624
+ # 16. 1.NoneType
625
+ # 17. 1.bool
626
+ # 18. 1.bytearray
627
+ # 19. 1.bytes
628
+ # 20. 1.complex
629
+ # 21. 1.dict
630
+ # 22. 1.float
631
+ # 23. 1.frozenset
632
+ # 24. 1.int
633
+ # 25. 1.list
634
+ # 26. 1.memoryview
635
+ # 27. 1.range
636
+ # 28. 1.set
637
+ # 29. 1.str
638
+ # 20. 1.tuple
639
+ # 21. json
640
+
641
+ Format C Type Python type Standard size
642
+ x pad byte no value
643
+ c char byte 1
644
+ b signed char integer 1
645
+ B unsigned char integer 1
646
+ ? _Bool bool 1
647
+ h short integer 2
648
+ H unsigned short integer 2
649
+ i int integer 4
650
+ I unsigned int integer 4
651
+ l long integer 4
652
+ L unsigned long integer 4
653
+ q long long integer 8
654
+ Q unsigned long long integer 8
655
+ n ssize_t integer
656
+ N size_t integer
657
+ e half float 2
658
+ f float float 4
659
+ d double float 8
660
+ s char[] bytes
661
+ p char[] bytes
662
+ P void* integer
663
+ """
664
+ # 17. 1.bool
665
+ if val == 1:
666
+ val = True
667
+ match ("1." if val else "0.") + type(val).__name__:
668
+ # 1. 0.NoneType
669
+ case "0.NoneType":
670
+ vtype = SQLITE_DATATYPE_NULL
671
+ vsize = 0
672
+ # 2. 0.bool
673
+ case "0.bool":
674
+ vtype = SQLITE_DATATYPE_INTEGER_0
675
+ vsize = 0
676
+ # 3. 0.bytearray
677
+ case "0.bytearray":
678
+ vtype = SQLITE_DATATYPE_NULL
679
+ vsize = 0
680
+ # 4. 0.bytes
681
+ case "0.bytes":
682
+ vtype = SQLITE_DATATYPE_NULL
683
+ vsize = 0
684
+ # 5. 0.complex
685
+ # 6. 0.dict
686
+ # 7. 0.float
687
+ case "0.float":
688
+ vtype = SQLITE_DATATYPE_INTEGER_0
689
+ vsize = 0
690
+ # 8. 0.frozenset
691
+ # 9. 0.int
692
+ case "0.int":
693
+ vtype = SQLITE_DATATYPE_INTEGER_0
694
+ vsize = 0
695
+ # 10. 0.list
696
+ # 11. 0.memoryview
697
+ case "0.memoryview":
698
+ vtype = SQLITE_DATATYPE_NULL
699
+ vsize = 0
700
+ # 12. 0.range
701
+ # 13. 0.set
702
+ # 14. 0.str
703
+ case "0.str":
704
+ vtype = SQLITE_DATATYPE_TEXT_0
705
+ vsize = 0
706
+ # 15. 0.tuple
707
+ # 16. 1.NoneType
708
+ # 17. 1.bool
709
+ case "1.bool":
710
+ vtype = SQLITE_DATATYPE_INTEGER_1
711
+ vsize = 0
712
+ # 18. 1.bytearray
713
+ case "1.bytearray":
714
+ vtype = SQLITE_DATATYPE_BLOB
715
+ vsize = 4 + len(val)
716
+ # 19. 1.bytes
717
+ case "1.bytes":
718
+ vtype = SQLITE_DATATYPE_BLOB
719
+ vsize = 4 + len(val)
720
+ # 20. 1.complex
721
+ # 21. 1.dict
722
+ # 22. 1.float
723
+ case "1.float":
724
+ if math.isnan(val):
725
+ vtype = SQLITE_DATATYPE_NULL
726
+ vsize = 0
727
+ else:
728
+ vtype = SQLITE_DATATYPE_FLOAT
729
+ vsize = 8
730
+ # 23. 1.frozenset
731
+ # 24. 1.int
732
+ case "1.int":
733
+ vtype = SQLITE_DATATYPE_INTEGER
734
+ vsize = 8
735
+ # 25. 1.list
736
+ # 26. 1.memoryview
737
+ case "1.memoryview":
738
+ vtype = SQLITE_DATATYPE_EXTERNALBUFFER
739
+ vsize = 4
740
+ # 27. 1.range
741
+ # 28. 1.set
742
+ # 29. 1.str
743
+ case "1.str":
744
+ val = bytes(val, "utf-8")
745
+ vtype = SQLITE_DATATYPE_TEXT
746
+ vsize = 4 + len(val)
747
+ # 30. 1.tuple
748
+ # 31. json
749
+ case _:
750
+ val = bytes(json.dumps(val), "utf-8")
751
+ vtype = SQLITE_DATATYPE_TEXT
752
+ vsize = 4 + len(val)
753
+ nused = struct.unpack_from("i", baton, 4)[0] # ctype-i = int
754
+ nn = nused + 1 + vsize
755
+ assert_or_throw(
756
+ nn <= 0xffff_ffff, # noqa: PLR2004
757
+ "jsbaton cannot exceed 0x7fff_ffff / 2,147,483,647 bytes",
758
+ )
759
+ # exponentially grow baton as needed
760
+ if len(baton) < nn:
761
+ tmp = baton
762
+ baton = memoryview(_sqlmath.PyBaton(
763
+ min(2 ** math.ceil(math.log2(nn)), 0x7fff_ffff),
764
+ ))
765
+ # update nallc
766
+ struct.pack_into("i", baton, 0, len(baton)) # ctype-i = int
767
+ # copy old-baton into new-baton
768
+ baton[:len(tmp)] = tmp
769
+ # push vtype - 1-byte
770
+ struct.pack_into("b", baton, nused, vtype) # ctype-b = signed-char
771
+ # update nused
772
+ struct.pack_into("i", baton, 4, nused + 1 + vsize) # ctype-i = int
773
+ # handle blob-value
774
+ if vtype in (SQLITE_DATATYPE_BLOB, SQLITE_DATATYPE_TEXT):
775
+ # set argv[ii] to blob/text location
776
+ if argi is not None:
777
+ # ctype-i = int
778
+ struct.pack_into("i", baton, JSBATON_OFFSET_ARGV + argi * 8, nused)
779
+ vsize -= 4
780
+ assert_or_throw(
781
+ 0 <= vsize <= SIZEOF_BLOB_MAX,
782
+ (
783
+ "sqlite-blob byte-length must be within inclusive-range"
784
+ f" 0 to {SIZEOF_BLOB_MAX}"
785
+ ),
786
+ )
787
+ # push vsize - 4-byte
788
+ struct.pack_into("i", baton, nused + 1, vsize) # ctype-i = int
789
+ # push SQLITE-BLOB/TEXT - vsize-byte
790
+ baton[nused + 1 + 4:nused + 1 + 4 + vsize] = val
791
+ return baton
792
+ if vtype == SQLITE_DATATYPE_EXTERNALBUFFER:
793
+ vsize = len(val)
794
+ assert_or_throw(
795
+ 0 <= vsize <= SIZEOF_BLOB_MAX,
796
+ (
797
+ "sqlite-blob byte-length must be within inclusive-range"
798
+ f" 0 to {SIZEOF_BLOB_MAX}"
799
+ ),
800
+ )
801
+ assert_or_throw(
802
+ bufi[0] < JSBATON_ARGC,
803
+ f"cannot pass more than {JSBATON_ARGC} arraybuffers",
804
+ )
805
+ # push externalbuffer - 4-byte
806
+ struct.pack_into("i", baton, nused + 1, bufi[0]) # ctype-i = int
807
+ # set buffer
808
+ _sqlmath.pybatonSetMemoryview(baton, bufi[0], val)
809
+ # increment bufi
810
+ bufi[0] += 1
811
+ # add buffer to reference_list to prevent gc during db_call.
812
+ reference_list.append(val)
813
+ return baton
814
+ if vtype == SQLITE_DATATYPE_FLOAT:
815
+ # push SQLITE-REAL - 8-byte
816
+ struct.pack_into("d", baton, nused + 1, val) # ctype-d = double
817
+ return baton
818
+ if vtype == SQLITE_DATATYPE_INTEGER:
819
+ assert_int64(val)
820
+ # push SQLITE-INTEGER - 8-byte
821
+ struct.pack_into("q", baton, nused + 1, val) # ctype-q = long-long
822
+ return baton
823
+ return baton
824
+
825
+
826
+ def noop(val=None, *_, **__):
827
+ """This function will do nothing except return <val>."""
828
+ return val
829
+
830
+
831
+ def lib_platform_arch_ext():
832
+ """This function will return f"{platform}_{arch}.{extension}"."""
833
+ lib_arch = (
834
+ platform.machine()
835
+ .lower()
836
+ .replace("aarch64", "arm64")
837
+ .replace("amd64", "x64")
838
+ .replace("x86_64", "x64")
839
+ )
840
+ lib_platform = sys.platform
841
+ lib_ext = "so"
842
+ if lib_platform == "darwin":
843
+ lib_ext = "dylib"
844
+ if lib_platform == "win32":
845
+ lib_ext = "dll"
846
+ return f"{lib_platform}_{lib_arch}.{lib_ext}"
847
+
848
+
849
+ def objectdeepcopywithkeyssorted(obj):
850
+ """This function will recursively deep-copy <obj> with keys sorted."""
851
+ if isinstance(obj, float):
852
+ return int(obj) if int(obj) == obj else obj
853
+ if not obj or not isinstance(obj, dict):
854
+ return obj
855
+ # Recursively deep-copy list with child-keys sorted.
856
+ if isinstance(obj, list):
857
+ return [objectdeepcopywithkeyssorted(elem) for elem in obj]
858
+ # Recursively deep-copy obj with keys sorted.
859
+ return dict(sorted((str(key), val) for key, val in obj.items()))