h2gis 2.0.0__py3-none-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.
h2gis/__init__.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ * H2GIS-python is a Python wrapper to use H2GIS.
3
+ * <a href="http://www.h2database.com">http://www.h2database.com</a>. H2GIS-python is developed by CNRS
4
+ * <a href="http://www.cnrs.fr/">http://www.cnrs.fr/</a>.
5
+ *
6
+ * This code is part of the H2GIS-python project. H2GIS-python is free software;
7
+ * you can redistribute it and/or modify it under the terms of the GNU
8
+ * Lesser General Public License as published by the Free Software Foundation;
9
+ * version 3.0 of the License.
10
+ *
11
+ * H2GIS-python is distributed in the hope that it will be useful, but
12
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
14
+ * for more details <http://www.gnu.org/licenses/>.
15
+ *
16
+ *
17
+ * For more information, please consult: <a href="http://www.h2gis.org/">http://www.h2gis.org/</a>
18
+ * or contact directly: info_at_h2gis.org
19
+
20
+ @author Maël PHILIPPE, CNRS
21
+ @author Erwan BOCHER, CNRS
22
+
23
+ DB-API 2.0 driver for H2GIS, with an API modelled on psycopg 3::
24
+
25
+ import h2gis
26
+ from h2gis.rows import dict_row
27
+
28
+ with h2gis.connect("/data/my_db", row_factory=dict_row) as conn:
29
+ conn.execute("INSERT INTO LIEUX(NOM, THE_GEOM) VALUES (%s, %s)", ["Nantes", point])
30
+ for row in conn.execute("SELECT * FROM LIEUX"):
31
+ print(row["NOM"], row["THE_GEOM"])
32
+ gdf = h2gis.read_gdf(conn, "SELECT * FROM LIEUX")
33
+ """
34
+
35
+ import datetime as _dt
36
+ import time as _time
37
+
38
+ from . import errors, rows, sql
39
+ from .adapt import AdaptersMap, ColumnType, Geometry, Json, adapters
40
+ from .connection import Connection, Rollback, Transaction, connect
41
+ from .cursor import Column, Cursor
42
+ from .dataframe import read_df, read_gdf, to_h2gis
43
+ from .errors import (DatabaseError, DataError, Error, IntegrityError, InterfaceError,
44
+ InternalError, NotSupportedError, OperationalError, ProgrammingError,
45
+ Warning)
46
+
47
+ __version__ = "2.0.0"
48
+
49
+ # --- PEP 249 module interface ------------------------------------------------
50
+
51
+ apilevel = "2.0"
52
+ threadsafety = 2 # threads may share the module and connections (calls are serialized)
53
+ paramstyle = "pyformat"
54
+
55
+
56
+ class DBAPITypeObject:
57
+ """Compare equal to the ``type_code`` values of a family of types (PEP 249)."""
58
+
59
+ def __init__(self, name: str, *codes: int):
60
+ self.name = name
61
+ self.codes = frozenset(codes)
62
+
63
+ def __eq__(self, other) -> bool:
64
+ if isinstance(other, DBAPITypeObject):
65
+ return self.codes == other.codes
66
+ return other in self.codes
67
+
68
+ def __hash__(self) -> int:
69
+ return hash(self.codes)
70
+
71
+ def __repr__(self) -> str:
72
+ return f"<DBAPITypeObject {self.name}>"
73
+
74
+
75
+ STRING = DBAPITypeObject("STRING", ColumnType.STRING)
76
+ BINARY = DBAPITypeObject("BINARY", ColumnType.BINARY)
77
+ NUMBER = DBAPITypeObject("NUMBER", ColumnType.INT, ColumnType.LONG, ColumnType.FLOAT,
78
+ ColumnType.DOUBLE, ColumnType.DECIMAL)
79
+ DATETIME = DBAPITypeObject("DATETIME", ColumnType.TEMPORAL)
80
+ ROWID = DBAPITypeObject("ROWID")
81
+ BOOLEAN = DBAPITypeObject("BOOLEAN", ColumnType.BOOLEAN)
82
+ GEOMETRY = DBAPITypeObject("GEOMETRY", ColumnType.GEOMETRY)
83
+ JSON = DBAPITypeObject("JSON", ColumnType.JSON)
84
+ UUID = DBAPITypeObject("UUID", ColumnType.UUID)
85
+
86
+ Date = _dt.date
87
+ Time = _dt.time
88
+ Timestamp = _dt.datetime
89
+ Binary = bytes
90
+
91
+
92
+ def DateFromTicks(ticks: float) -> _dt.date:
93
+ return Date(*_time.localtime(ticks)[:3])
94
+
95
+
96
+ def TimeFromTicks(ticks: float) -> _dt.time:
97
+ return Time(*_time.localtime(ticks)[3:6])
98
+
99
+
100
+ def TimestampFromTicks(ticks: float) -> _dt.datetime:
101
+ return Timestamp(*_time.localtime(ticks)[:6])
102
+
103
+
104
+ __all__ = [
105
+ "connect", "Connection", "Cursor", "Column", "Transaction", "Rollback",
106
+ "read_df", "read_gdf", "to_h2gis", "Geometry", "Json", "AdaptersMap", "adapters",
107
+ "errors", "rows", "sql",
108
+ "apilevel", "threadsafety", "paramstyle",
109
+ "Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError",
110
+ "IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError",
111
+ "STRING", "BINARY", "NUMBER", "DATETIME", "ROWID", "BOOLEAN", "GEOMETRY", "JSON", "UUID",
112
+ "Date", "Time", "Timestamp", "Binary", "DateFromTicks", "TimeFromTicks",
113
+ "TimestampFromTicks",
114
+ ]
h2gis/_buffer.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ H2GIS-python is a Python wrapper to use H2GIS <http://www.h2gis.org/>.
3
+ H2GIS-python is developed by CNRS <http://www.cnrs.fr/>.
4
+
5
+ This code is part of the H2GIS-python project. H2GIS-python is free software;
6
+ you can redistribute it and/or modify it under the terms of the GNU
7
+ Lesser General Public License as published by the Free Software Foundation;
8
+ version 3.0 of the License. See <http://www.gnu.org/licenses/>.
9
+
10
+ Decoder of the column oriented buffer produced by
11
+ ``org.h2gis.graalvm.ResultSetWrapper.serialize()`` (all integers little endian)::
12
+
13
+ u32 column_count
14
+ u32 row_count
15
+ u64 column_offset[column_count] # from the start of the buffer
16
+ column*:
17
+ u32 name_length, utf-8 name
18
+ u32 type_code # see adapt.ColumnType, | 0x100 if a null mask follows
19
+ [u8 null_mask[ceil(row_count/8)]] # bit i % 8 of byte i // 8 set: row i is NULL
20
+ u32 values_length
21
+ values:
22
+ INT i32 | LONG i64 | FLOAT f32 | DOUBLE f64 | BOOLEAN u8
23
+ other types: u32 length + bytes per value
24
+
25
+ NULL values keep a placeholder in ``values``; the mask tells them apart.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import struct
31
+ from typing import List
32
+
33
+ from . import errors
34
+ from .adapt import ColumnType
35
+
36
+ NULL_LENGTH = 0xFFFFFFFF
37
+ FLAG_NULL_MASK = 0x100
38
+
39
+ _FIXED = {
40
+ ColumnType.INT: "i",
41
+ ColumnType.LONG: "q",
42
+ ColumnType.FLOAT: "f",
43
+ ColumnType.DOUBLE: "d",
44
+ }
45
+
46
+
47
+ class Chunk:
48
+ """A decoded buffer: column names, type codes and values by column."""
49
+
50
+ __slots__ = ("names", "type_codes", "row_count", "columns", "has_null_mask")
51
+
52
+ def __init__(self, names, type_codes, row_count, columns, has_null_mask):
53
+ self.names: List[str] = names
54
+ self.type_codes: List[int] = type_codes
55
+ self.row_count: int = row_count
56
+ self.columns: List[list] = columns
57
+ self.has_null_mask: List[bool] = has_null_mask
58
+
59
+
60
+ def decode(buf: bytes) -> Chunk:
61
+ view = memoryview(buf)
62
+ try:
63
+ col_count, row_count = struct.unpack_from("<II", view, 0)
64
+ offsets = struct.unpack_from(f"<{col_count}Q", view, 8)
65
+ names, codes, columns, masks = [], [], [], []
66
+ for offset in offsets:
67
+ name_len = struct.unpack_from("<I", view, offset)[0]
68
+ pos = offset + 4
69
+ names.append(bytes(view[pos:pos + name_len]).decode("utf-8", "replace"))
70
+ pos += name_len
71
+ raw_code = struct.unpack_from("<I", view, pos)[0]
72
+ pos += 4
73
+ code = raw_code & ~FLAG_NULL_MASK
74
+ nulls = None
75
+ if raw_code & FLAG_NULL_MASK:
76
+ mask_len = (row_count + 7) // 8
77
+ nulls = bytes(view[pos:pos + mask_len])
78
+ pos += mask_len
79
+ values_len = struct.unpack_from("<I", view, pos)[0]
80
+ pos += 4
81
+ values = _decode_values(view[pos:pos + values_len], code, row_count)
82
+ if nulls is not None:
83
+ for i in range(row_count):
84
+ if nulls[i >> 3] & (1 << (i & 7)):
85
+ values[i] = None
86
+ codes.append(code)
87
+ columns.append(values)
88
+ masks.append(nulls is not None)
89
+ except (struct.error, IndexError, UnicodeDecodeError) as exc:
90
+ raise errors.InterfaceError(f"malformed result buffer: {exc}") from exc
91
+ return Chunk(names, codes, row_count, columns, masks)
92
+
93
+
94
+ def _decode_values(view: memoryview, code: int, n: int) -> list:
95
+ fmt = _FIXED.get(code)
96
+ if fmt is not None:
97
+ return list(struct.unpack_from(f"<{n}{fmt}", view, 0)) if n else []
98
+ if code == ColumnType.BOOLEAN:
99
+ return [b != 0 for b in bytes(view[:n])]
100
+ out: list = []
101
+ pos = 0
102
+ as_text = code not in (ColumnType.GEOMETRY, ColumnType.BINARY)
103
+ for _ in range(n):
104
+ length = struct.unpack_from("<I", view, pos)[0]
105
+ pos += 4
106
+ if length == NULL_LENGTH:
107
+ out.append(None)
108
+ continue
109
+ data = bytes(view[pos:pos + length])
110
+ if len(data) != length:
111
+ raise errors.InterfaceError("malformed result buffer: truncated value")
112
+ pos += length
113
+ out.append(data.decode("utf-8", "replace") if as_text else data)
114
+ return out
h2gis/_native.py ADDED
@@ -0,0 +1,368 @@
1
+ """
2
+ H2GIS-python is a Python wrapper to use H2GIS <http://www.h2gis.org/>.
3
+ H2GIS-python is developed by CNRS <http://www.cnrs.fr/>.
4
+
5
+ This code is part of the H2GIS-python project. H2GIS-python is free software;
6
+ you can redistribute it and/or modify it under the terms of the GNU
7
+ Lesser General Public License as published by the Free Software Foundation;
8
+ version 3.0 of the License. See <http://www.gnu.org/licenses/>.
9
+
10
+ Low level binding to the GraalVM native library (``h2gis.so`` / ``.dll`` /
11
+ ``.dylib``) built from ``org.h2gis.graalvm.GraalCInterface`` (module
12
+ ``h2gis-graalvm`` of H2GIS).
13
+
14
+ * The library must implement the C API version 2 or later
15
+ (``h2gis_version()``); see docs/NATIVE_API.md.
16
+ * One GraalVM isolate per library and per process, shared by every connection.
17
+ * Each Python thread gets its own isolate thread (``graal_attach_thread``),
18
+ detached when the thread ends: calling into an isolate with the isolate
19
+ thread of another OS thread is undefined behaviour, and the Java side keeps
20
+ the last error per thread.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import ctypes
26
+ import json
27
+ import os
28
+ import platform
29
+ import threading
30
+ from typing import Dict, List, Optional
31
+
32
+ from . import errors
33
+
34
+ __all__ = ["NativeLib", "get_lib", "default_library_path", "native_platform",
35
+ "library_file_name", "MIN_C_API_VERSION"]
36
+
37
+ #: Oldest version of the C interface this driver works with.
38
+ MIN_C_API_VERSION = 2
39
+
40
+
41
+ class _GraalIsolate(ctypes.Structure):
42
+ _fields_: list = []
43
+
44
+
45
+ class _GraalIsolateThread(ctypes.Structure):
46
+ _fields_: list = []
47
+
48
+
49
+ _Isolate_p = ctypes.POINTER(_GraalIsolate)
50
+ _Thread_p = ctypes.POINTER(_GraalIsolateThread)
51
+
52
+ # Java long is 64 bits; C long is only 32 bits on Windows (LLP64)
53
+ _jlong = ctypes.c_int64
54
+ _jint = ctypes.c_int32
55
+ _c_char_p = ctypes.c_char_p
56
+ _c_void_p = ctypes.c_void_p
57
+
58
+ # name -> (argtypes, restype). The isolate thread is always the first argument.
59
+ _FUNCTIONS = {
60
+ "h2gis_version": ([_Thread_p], _jint),
61
+ "h2gis_get_last_error": ([_Thread_p], _c_void_p),
62
+ "h2gis_free_result_buffer": ([_Thread_p, _c_void_p], None),
63
+ "h2gis_connect": ([_Thread_p, _c_char_p, _c_char_p, _c_char_p], _jlong),
64
+ "h2gis_load": ([_Thread_p, _jlong], _jlong),
65
+ "h2gis_close_connection": ([_Thread_p, _jlong], None),
66
+ "h2gis_delete_database_and_close": ([_Thread_p, _jlong], None),
67
+ "h2gis_execute": ([_Thread_p, _jlong, _c_char_p], _jint),
68
+ "h2gis_fetch": ([_Thread_p, _jlong, _c_char_p], _jlong),
69
+ "h2gis_prepare": ([_Thread_p, _jlong, _c_char_p], _jlong),
70
+ "h2gis_bind_long": ([_Thread_p, _jlong, _jint, _jlong], _jint),
71
+ "h2gis_bind_double": ([_Thread_p, _jlong, _jint, ctypes.c_double], _jint),
72
+ "h2gis_bind_string": ([_Thread_p, _jlong, _jint, _c_char_p], _jint),
73
+ "h2gis_bind_blob": ([_Thread_p, _jlong, _jint, _c_char_p, _jint], _jint),
74
+ "h2gis_bind_null": ([_Thread_p, _jlong, _jint], _jint),
75
+ "h2gis_execute_prepared": ([_Thread_p, _jlong], _jlong),
76
+ "h2gis_execute_prepared_update": ([_Thread_p, _jlong], _jint),
77
+ "h2gis_add_batch": ([_Thread_p, _jlong], _jint),
78
+ "h2gis_execute_batch": ([_Thread_p, _jlong], _jlong),
79
+ "h2gis_close_query": ([_Thread_p, _jlong], None),
80
+ "h2gis_fetch_all": ([_Thread_p, _jlong, _c_void_p], _c_void_p),
81
+ "h2gis_fetch_batch": ([_Thread_p, _jlong, _jint, _c_void_p], _c_void_p),
82
+ "h2gis_get_metadata_json": ([_Thread_p, _jlong], _c_void_p),
83
+ }
84
+
85
+
86
+ _LIB_NAMES = {"Windows": "h2gis.dll", "Darwin": "h2gis.dylib"}
87
+
88
+
89
+ def native_platform() -> str:
90
+ """Name of the current platform in the ``native/`` folder: ``linux-x86_64``,
91
+ ``windows-x86_64``, ``macos-arm64``..."""
92
+ system = {"Darwin": "macos"}.get(platform.system(), platform.system().lower())
93
+ machine = platform.machine().lower()
94
+ machine = {"amd64": "x86_64", "x64": "x86_64", "aarch64": "arm64" if system == "macos" else "aarch64"
95
+ }.get(machine, machine)
96
+ return f"{system}-{machine}"
97
+
98
+
99
+ def library_file_name() -> str:
100
+ """``h2gis.so``, ``h2gis.dll`` or ``h2gis.dylib``."""
101
+ return _LIB_NAMES.get(platform.system(), "h2gis.so")
102
+
103
+
104
+ def default_library_path() -> str:
105
+ """Path of the native library, the first of:
106
+
107
+ 1. ``$H2GIS_LIB_PATH``;
108
+ 2. ``h2gis/lib/h2gis.<ext>``, inside an installed wheel;
109
+ 3. ``native/<platform>/h2gis.<ext>`` of the repository, when h2gis is used
110
+ from a clone (``pip install -e .``, tests).
111
+ """
112
+ env = os.environ.get("H2GIS_LIB_PATH")
113
+ if env:
114
+ return env
115
+ package_dir = os.path.dirname(os.path.abspath(__file__))
116
+ name = library_file_name()
117
+ bundled = os.path.join(package_dir, "lib", name)
118
+ if os.path.exists(bundled):
119
+ return bundled
120
+ checkout = os.path.join(os.path.dirname(package_dir), "native", native_platform(), name)
121
+ if os.path.exists(checkout):
122
+ return checkout
123
+ return bundled # missing: NativeLib reports the expected location
124
+
125
+
126
+ class NativeLib:
127
+ """A loaded native library and its GraalVM isolate.
128
+
129
+ Don't instantiate directly: use :func:`get_lib`, which returns one shared
130
+ instance per library path.
131
+ """
132
+
133
+ def __init__(self, path: str):
134
+ if not os.path.exists(path):
135
+ raise errors.InterfaceError(
136
+ f"native H2GIS library not found: {path!r}. Install the h2gis "
137
+ "package with its 'lib' folder, set H2GIS_LIB_PATH or pass lib_path=; "
138
+ "from a clone: python tools/update_native.py --download --current-platform")
139
+ self.path = path
140
+ try:
141
+ self._lib = ctypes.CDLL(path)
142
+ except OSError as exc:
143
+ raise errors.InterfaceError(f"cannot load native library {path!r}: {exc}") from exc
144
+
145
+ if getattr(self._lib, "h2gis_version", None) is None:
146
+ raise errors.InterfaceError(
147
+ f"{path!r} implements the H2GIS C API version 1; h2gis-python 2 needs "
148
+ f"version {MIN_C_API_VERSION} or later (H2GIS >= 2.2.6)")
149
+ missing = [name for name in _FUNCTIONS if getattr(self._lib, name, None) is None]
150
+ if missing:
151
+ raise errors.InterfaceError(f"{path!r} has no entry point {', '.join(missing)}")
152
+ for name, (args, res) in _FUNCTIONS.items():
153
+ fn = getattr(self._lib, name)
154
+ fn.argtypes, fn.restype = args, res
155
+
156
+ self._lib.graal_create_isolate.argtypes = [
157
+ _c_void_p, ctypes.POINTER(_Isolate_p), ctypes.POINTER(_Thread_p)]
158
+ self._lib.graal_create_isolate.restype = ctypes.c_int
159
+ self._lib.graal_attach_thread.argtypes = [_Isolate_p, ctypes.POINTER(_Thread_p)]
160
+ self._lib.graal_attach_thread.restype = ctypes.c_int
161
+ self._lib.graal_detach_thread.argtypes = [_Thread_p]
162
+ self._lib.graal_detach_thread.restype = ctypes.c_int
163
+
164
+ self._isolate = _Isolate_p()
165
+ thread = _Thread_p()
166
+ rc = self._lib.graal_create_isolate(None, ctypes.byref(self._isolate), ctypes.byref(thread))
167
+ if rc != 0:
168
+ raise errors.InterfaceError(f"failed to create the GraalVM isolate (code {rc})")
169
+ self._local = threading.local()
170
+ self._local.thread = thread
171
+
172
+ self.version: int = self._lib.h2gis_version(thread)
173
+ if self.version < MIN_C_API_VERSION:
174
+ raise errors.InterfaceError(
175
+ f"{path!r} implements the H2GIS C API version {self.version}; "
176
+ f"h2gis-python 2 needs version {MIN_C_API_VERSION} or later")
177
+
178
+ # -- isolate thread -------------------------------------------------------
179
+
180
+ @property
181
+ def thread(self):
182
+ thread = getattr(self._local, "thread", None)
183
+ if thread is None:
184
+ thread = _Thread_p()
185
+ rc = self._lib.graal_attach_thread(self._isolate, ctypes.byref(thread))
186
+ if rc != 0:
187
+ raise errors.InterfaceError(f"failed to attach thread to the isolate (code {rc})")
188
+ self._local.thread = thread
189
+ # Detach when the Python thread ends: CPython clears the thread
190
+ # local storage from the exiting thread itself, which is what
191
+ # graal_detach_thread requires. A thread that exits while still
192
+ # attached crashes the isolate at its next safepoint.
193
+ self._local.detacher = _Detacher(self._lib, thread)
194
+ return thread
195
+
196
+ # -- strings, errors ------------------------------------------------------
197
+
198
+ def _take_string(self, ptr) -> str:
199
+ """Read and free a string allocated by the library."""
200
+ if not ptr:
201
+ return ""
202
+ try:
203
+ return ctypes.string_at(ptr).decode("utf-8", "replace")
204
+ finally:
205
+ self._lib.h2gis_free_result_buffer(self.thread, ptr)
206
+
207
+ def last_error(self) -> str:
208
+ return self._take_string(self._lib.h2gis_get_last_error(self.thread))
209
+
210
+ def _raise(self, what: str, default=errors.DatabaseError):
211
+ message = self.last_error()
212
+ if not message:
213
+ raise default(f"{what} failed (no error message from the native library)")
214
+ raise errors.from_native_message(message, default)
215
+
216
+ # -- connections ----------------------------------------------------------
217
+
218
+ def connect(self, path: str, user: str, password: str) -> int:
219
+ handle = self._lib.h2gis_connect(self.thread, _encode(path), _encode(user), _encode(password))
220
+ if not handle:
221
+ self._raise("connection", errors.OperationalError)
222
+ return handle
223
+
224
+ def close_connection(self, conn: int) -> None:
225
+ self._lib.h2gis_close_connection(self.thread, conn)
226
+
227
+ def delete_database_and_close(self, conn: int) -> None:
228
+ self._lib.h2gis_delete_database_and_close(self.thread, conn)
229
+ message = self.last_error()
230
+ if message:
231
+ raise errors.from_native_message(message, errors.OperationalError)
232
+
233
+ def load_functions(self, conn: int) -> None:
234
+ """Register the H2GIS spatial functions (done by connect() when missing)."""
235
+ if not self._lib.h2gis_load(self.thread, conn):
236
+ self._raise("loading the H2GIS functions")
237
+
238
+ # -- plain statements -----------------------------------------------------
239
+
240
+ def execute(self, conn: int, sql: str) -> int:
241
+ """Run an update statement (or a script), return the update count."""
242
+ count = self._lib.h2gis_execute(self.thread, conn, _encode(sql))
243
+ if count < 0:
244
+ self._raise("execute")
245
+ return count
246
+
247
+ def query(self, conn: int, sql: str) -> int:
248
+ """Run a query, return a result set handle."""
249
+ handle = self._lib.h2gis_fetch(self.thread, conn, _encode(sql))
250
+ if not handle:
251
+ self._raise("query")
252
+ return handle
253
+
254
+ def close_query(self, handle: int) -> None:
255
+ """Close a result set or statement handle (and the results of a statement)."""
256
+ self._lib.h2gis_close_query(self.thread, handle)
257
+
258
+ # -- prepared statements --------------------------------------------------
259
+
260
+ def prepare(self, conn: int, sql: str) -> int:
261
+ handle = self._lib.h2gis_prepare(self.thread, conn, _encode(sql))
262
+ if not handle:
263
+ self._raise("prepare", errors.ProgrammingError)
264
+ return handle
265
+
266
+ def bind(self, stmt: int, index: int, kind: str, value) -> None:
267
+ """Bind one parameter (1-based ``index``) as returned by ``adapt.bind_value``."""
268
+ lib, th = self._lib, self.thread
269
+ if kind == "null":
270
+ rc = lib.h2gis_bind_null(th, stmt, index)
271
+ elif kind == "long":
272
+ rc = lib.h2gis_bind_long(th, stmt, index, value)
273
+ elif kind == "double":
274
+ rc = lib.h2gis_bind_double(th, stmt, index, value)
275
+ elif kind == "string":
276
+ rc = lib.h2gis_bind_string(th, stmt, index, value.encode("utf-8"))
277
+ elif kind == "blob":
278
+ data = bytes(value)
279
+ rc = lib.h2gis_bind_blob(th, stmt, index, data, len(data))
280
+ else: # pragma: no cover - guarded by adapt.bind_value
281
+ raise errors.InterfaceError(f"unknown bind kind {kind!r}")
282
+ if rc != 0:
283
+ self._raise(f"binding parameter {index}", errors.DataError)
284
+
285
+ def execute_prepared(self, stmt: int) -> int:
286
+ handle = self._lib.h2gis_execute_prepared(self.thread, stmt)
287
+ if not handle:
288
+ self._raise("prepared query")
289
+ return handle
290
+
291
+ def execute_prepared_update(self, stmt: int) -> int:
292
+ count = self._lib.h2gis_execute_prepared_update(self.thread, stmt)
293
+ if count < 0:
294
+ self._raise("prepared update")
295
+ return count
296
+
297
+ def add_batch(self, stmt: int) -> None:
298
+ if self._lib.h2gis_add_batch(self.thread, stmt) != 0:
299
+ self._raise("add batch")
300
+
301
+ def execute_batch(self, stmt: int) -> int:
302
+ count = self._lib.h2gis_execute_batch(self.thread, stmt)
303
+ if count < 0:
304
+ self._raise("execute batch")
305
+ return count
306
+
307
+ # -- results --------------------------------------------------------------
308
+
309
+ def _buffer(self, fn, what: str, *args) -> bytes:
310
+ size = ctypes.c_int64(0)
311
+ ptr = fn(self.thread, *args, ctypes.byref(size))
312
+ if not ptr:
313
+ self._raise(what, errors.FetchError)
314
+ try:
315
+ return ctypes.string_at(ptr, size.value)
316
+ finally:
317
+ self._lib.h2gis_free_result_buffer(self.thread, ptr)
318
+
319
+ def fetch_all(self, rs: int) -> bytes:
320
+ """Serialized remaining rows (see _buffer.py)."""
321
+ return self._buffer(self._lib.h2gis_fetch_all, "fetch", rs)
322
+
323
+ def fetch_batch(self, rs: int, size: int) -> bytes:
324
+ """Serialized next ``size`` rows; fewer rows means the end is reached."""
325
+ return self._buffer(self._lib.h2gis_fetch_batch, "fetch", rs, int(size))
326
+
327
+ def metadata(self, rs: int) -> List[dict]:
328
+ """Column metadata: ``[{"name", "typeName", "type"}, ...]``."""
329
+ raw = self._take_string(self._lib.h2gis_get_metadata_json(self.thread, rs))
330
+ try:
331
+ return json.loads(raw) if raw else []
332
+ except ValueError:
333
+ return []
334
+
335
+
336
+ class _Detacher:
337
+ __slots__ = ("lib", "thread")
338
+
339
+ def __init__(self, lib, thread):
340
+ self.lib, self.thread = lib, thread
341
+
342
+ def __del__(self):
343
+ try:
344
+ self.lib.graal_detach_thread(self.thread)
345
+ except Exception: # pragma: no cover - interpreter shutdown
346
+ pass
347
+
348
+
349
+ def _encode(text: str) -> bytes:
350
+ if not isinstance(text, str):
351
+ raise errors.InterfaceError(f"expected str, got {type(text).__name__}")
352
+ if "\x00" in text:
353
+ raise errors.DataError("NUL characters are not allowed in SQL strings")
354
+ return text.encode("utf-8")
355
+
356
+
357
+ _LIBS: Dict[str, NativeLib] = {}
358
+ _LIBS_LOCK = threading.Lock()
359
+
360
+
361
+ def get_lib(path: Optional[str] = None) -> NativeLib:
362
+ """Return the shared :class:`NativeLib` for ``path`` (default library if None)."""
363
+ path = os.path.realpath(path or default_library_path())
364
+ with _LIBS_LOCK:
365
+ lib = _LIBS.get(path)
366
+ if lib is None:
367
+ lib = _LIBS[path] = NativeLib(path)
368
+ return lib