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()))
|