ownSQL 1.0.0__py3-none-any.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.
- ownSQL.py +567 -0
- ownsql-1.0.0.dist-info/METADATA +169 -0
- ownsql-1.0.0.dist-info/RECORD +6 -0
- ownsql-1.0.0.dist-info/WHEEL +5 -0
- ownsql-1.0.0.dist-info/licenses/LICENSE +22 -0
- ownsql-1.0.0.dist-info/top_level.txt +1 -0
ownSQL.py
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
# MIT License
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2026 TimonW-Dev
|
|
5
|
+
# Copyright (c) 2014-2019, 2021-2026 Hajime Nakagami
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
# copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
# SOFTWARE.
|
|
24
|
+
#########################################################################################
|
|
25
|
+
# ownSQL is a MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.
|
|
26
|
+
# It was derived from micropg_lite and adapted from PostgreSQL to MySQL with the
|
|
27
|
+
# assistance of AI tools. micropg, the original project on which micropg_lite is based,
|
|
28
|
+
# was developed by Hajime Nakagami.
|
|
29
|
+
#########################################################################################
|
|
30
|
+
|
|
31
|
+
import socket
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
CLIENT_LONG_PASSWORD = 0x00000001
|
|
35
|
+
CLIENT_LONG_FLAG = 0x00000004
|
|
36
|
+
CLIENT_CONNECT_WITH_DB = 0x00000008
|
|
37
|
+
CLIENT_PROTOCOL_41 = 0x00000200
|
|
38
|
+
CLIENT_SSL = 0x00000800
|
|
39
|
+
CLIENT_TRANSACTIONS = 0x00002000
|
|
40
|
+
CLIENT_SECURE_CONNECTION = 0x00008000
|
|
41
|
+
CLIENT_PLUGIN_AUTH = 0x00080000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Error(Exception):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class OperationalError(Error):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProgrammingError(Error):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _lost():
|
|
57
|
+
raise OperationalError("MySQL: lost connection")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _lenenc_int(data, pos=0):
|
|
61
|
+
if pos >= len(data):
|
|
62
|
+
raise OperationalError("MySQL: truncated length-encoded integer")
|
|
63
|
+
first = data[pos]
|
|
64
|
+
pos += 1
|
|
65
|
+
if first < 0xfb:
|
|
66
|
+
return first, pos
|
|
67
|
+
if first == 0xfc:
|
|
68
|
+
if len(data) < pos + 2:
|
|
69
|
+
raise OperationalError("MySQL: truncated length-encoded integer")
|
|
70
|
+
return int.from_bytes(data[pos:pos + 2], "little"), pos + 2
|
|
71
|
+
if first == 0xfd:
|
|
72
|
+
if len(data) < pos + 3:
|
|
73
|
+
raise OperationalError("MySQL: truncated length-encoded integer")
|
|
74
|
+
return int.from_bytes(data[pos:pos + 3], "little"), pos + 3
|
|
75
|
+
if first == 0xfe:
|
|
76
|
+
if len(data) < pos + 8:
|
|
77
|
+
raise OperationalError("MySQL: truncated length-encoded integer")
|
|
78
|
+
return int.from_bytes(data[pos:pos + 8], "little"), pos + 8
|
|
79
|
+
if first == 0xfb:
|
|
80
|
+
return None, pos
|
|
81
|
+
raise OperationalError("MySQL: invalid length-encoded integer")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _lenenc_bytes(data, pos=0):
|
|
85
|
+
size, pos = _lenenc_int(data, pos)
|
|
86
|
+
if size is None:
|
|
87
|
+
return None, pos
|
|
88
|
+
if size > len(data) - pos:
|
|
89
|
+
raise OperationalError("MySQL: truncated length-encoded value")
|
|
90
|
+
return data[pos:pos + size], pos + size
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _sha256(data):
|
|
94
|
+
import hashlib
|
|
95
|
+
return hashlib.sha256(data).digest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _auth_response(plugin, password, salt, tls_active=False):
|
|
99
|
+
if plugin == "sha256_password":
|
|
100
|
+
if not tls_active:
|
|
101
|
+
raise OperationalError(
|
|
102
|
+
"MySQL: sha256_password authentication requires use_ssl=True"
|
|
103
|
+
)
|
|
104
|
+
return password.encode("utf-8") + b"\x00"
|
|
105
|
+
if not password:
|
|
106
|
+
return b""
|
|
107
|
+
raw_password = password.encode("utf-8")
|
|
108
|
+
if plugin == "caching_sha2_password":
|
|
109
|
+
stage1 = _sha256(raw_password)
|
|
110
|
+
stage2 = _sha256(stage1)
|
|
111
|
+
stage3 = _sha256(salt + stage2)
|
|
112
|
+
return bytes(a ^ b for a, b in zip(stage1, stage3))
|
|
113
|
+
if plugin == "mysql_native_password":
|
|
114
|
+
import hashlib
|
|
115
|
+
stage1 = hashlib.sha1(raw_password).digest()
|
|
116
|
+
stage2 = hashlib.sha1(stage1).digest()
|
|
117
|
+
stage3 = hashlib.sha1(salt + stage2).digest()
|
|
118
|
+
return bytes(a ^ b for a, b in zip(stage1, stage3))
|
|
119
|
+
raise OperationalError("MySQL: unsupported authentication plugin: " + plugin)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _query_with_params(query, params):
|
|
123
|
+
if not isinstance(params, (tuple, list)):
|
|
124
|
+
raise ProgrammingError("MySQL parameters must be a tuple or list")
|
|
125
|
+
|
|
126
|
+
result = []
|
|
127
|
+
param_index = 0
|
|
128
|
+
pos = 0
|
|
129
|
+
while pos < len(query):
|
|
130
|
+
char = query[pos]
|
|
131
|
+
if char == "%" and pos + 1 < len(query):
|
|
132
|
+
marker = query[pos + 1]
|
|
133
|
+
if marker == "%":
|
|
134
|
+
result.append("%")
|
|
135
|
+
pos += 2
|
|
136
|
+
continue
|
|
137
|
+
if marker == "s":
|
|
138
|
+
if param_index >= len(params):
|
|
139
|
+
raise ProgrammingError("not enough parameters for query")
|
|
140
|
+
result.append(_sql_literal(params[param_index]))
|
|
141
|
+
param_index += 1
|
|
142
|
+
pos += 2
|
|
143
|
+
continue
|
|
144
|
+
result.append(char)
|
|
145
|
+
pos += 1
|
|
146
|
+
|
|
147
|
+
if param_index != len(params):
|
|
148
|
+
raise ProgrammingError("too many parameters for query")
|
|
149
|
+
return "".join(result)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _sql_literal(value):
|
|
153
|
+
if value is None:
|
|
154
|
+
return "NULL"
|
|
155
|
+
if value is True:
|
|
156
|
+
return "1"
|
|
157
|
+
if value is False:
|
|
158
|
+
return "0"
|
|
159
|
+
if isinstance(value, (int, float)):
|
|
160
|
+
return str(value)
|
|
161
|
+
import binascii
|
|
162
|
+
if isinstance(value, str):
|
|
163
|
+
encoded = value.encode("utf-8")
|
|
164
|
+
hex_value = binascii.hexlify(encoded).decode("ascii")
|
|
165
|
+
return "CONVERT(X'" + hex_value + "' USING utf8mb4)"
|
|
166
|
+
if isinstance(value, (bytes, bytearray)):
|
|
167
|
+
hex_value = binascii.hexlify(bytes(value)).decode("ascii")
|
|
168
|
+
return "X'" + hex_value + "'"
|
|
169
|
+
raise ProgrammingError("unsupported MySQL parameter type: " + type(value).__name__)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Cursor:
|
|
173
|
+
def __init__(self, connection):
|
|
174
|
+
self.connection = connection
|
|
175
|
+
self.description = None
|
|
176
|
+
self.rowcount = -1
|
|
177
|
+
self.lastrowid = 0
|
|
178
|
+
self._rows = []
|
|
179
|
+
|
|
180
|
+
def execute(self, query, params=()):
|
|
181
|
+
if not self.connection:
|
|
182
|
+
raise OperationalError("MySQL: cursor is closed")
|
|
183
|
+
if not isinstance(query, str):
|
|
184
|
+
raise ProgrammingError("MySQL query must be a string")
|
|
185
|
+
self.description = None
|
|
186
|
+
self.rowcount = -1
|
|
187
|
+
self.lastrowid = 0
|
|
188
|
+
self._rows = []
|
|
189
|
+
query = _query_with_params(query, params)
|
|
190
|
+
self.connection.execute(query, self)
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def fetchall(self):
|
|
194
|
+
rows = self._rows
|
|
195
|
+
self._rows = []
|
|
196
|
+
return rows
|
|
197
|
+
|
|
198
|
+
def close(self):
|
|
199
|
+
self.connection = None
|
|
200
|
+
self._rows = []
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class connect:
|
|
204
|
+
def __init__(self, host, user, password, database=None, port=3306, use_ssl=True):
|
|
205
|
+
self.encoding = "utf-8"
|
|
206
|
+
self.autocommit = False
|
|
207
|
+
self.sock = None
|
|
208
|
+
self.host = host
|
|
209
|
+
self._password = password
|
|
210
|
+
self._use_ssl = use_ssl
|
|
211
|
+
self._tls_active = False
|
|
212
|
+
self._seq = 0
|
|
213
|
+
self._in_transaction = False
|
|
214
|
+
|
|
215
|
+
sock = socket.socket()
|
|
216
|
+
try:
|
|
217
|
+
address = socket.getaddrinfo(host, port)[0][-1]
|
|
218
|
+
sock.connect(address)
|
|
219
|
+
self.sock = sock
|
|
220
|
+
self._handshake(user, password, database)
|
|
221
|
+
except Exception:
|
|
222
|
+
try:
|
|
223
|
+
sock.close()
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
self.sock = None
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
def _read(self, size):
|
|
230
|
+
sock = self.sock
|
|
231
|
+
if not sock:
|
|
232
|
+
_lost()
|
|
233
|
+
result = b""
|
|
234
|
+
while len(result) < size:
|
|
235
|
+
read = sock.read if hasattr(sock, "read") else sock.recv
|
|
236
|
+
chunk = read(size - len(result))
|
|
237
|
+
if not chunk:
|
|
238
|
+
_lost()
|
|
239
|
+
result += chunk
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
def _write(self, data):
|
|
243
|
+
sock = self.sock
|
|
244
|
+
if not sock:
|
|
245
|
+
_lost()
|
|
246
|
+
write = sock.write if hasattr(sock, "write") else sock.send
|
|
247
|
+
view = memoryview(data)
|
|
248
|
+
offset = 0
|
|
249
|
+
while offset < len(data):
|
|
250
|
+
written = write(view[offset:])
|
|
251
|
+
if not written:
|
|
252
|
+
_lost()
|
|
253
|
+
offset += written
|
|
254
|
+
|
|
255
|
+
def _read_packet(self):
|
|
256
|
+
chunks = []
|
|
257
|
+
while True:
|
|
258
|
+
header = self._read(4)
|
|
259
|
+
size = header[0] | (header[1] << 8) | (header[2] << 16)
|
|
260
|
+
seq = header[3]
|
|
261
|
+
if seq != self._seq:
|
|
262
|
+
raise OperationalError("MySQL: invalid packet sequence")
|
|
263
|
+
self._seq = (self._seq + 1) & 0xff
|
|
264
|
+
chunks.append(self._read(size))
|
|
265
|
+
if size < 0xffffff:
|
|
266
|
+
return b"".join(chunks)
|
|
267
|
+
|
|
268
|
+
def _send_packet(self, payload):
|
|
269
|
+
offset = 0
|
|
270
|
+
size = len(payload)
|
|
271
|
+
while size - offset >= 0xffffff:
|
|
272
|
+
part = payload[offset:offset + 0xffffff]
|
|
273
|
+
self._write(bytes((0xff, 0xff, 0xff, self._seq)) + part)
|
|
274
|
+
self._seq = (self._seq + 1) & 0xff
|
|
275
|
+
offset += 0xffffff
|
|
276
|
+
tail = payload[offset:]
|
|
277
|
+
length = len(tail)
|
|
278
|
+
header = bytes((length & 0xff, (length >> 8) & 0xff, (length >> 16) & 0xff, self._seq))
|
|
279
|
+
self._write(header + tail)
|
|
280
|
+
self._seq = (self._seq + 1) & 0xff
|
|
281
|
+
|
|
282
|
+
def _handshake(self, user, password, database):
|
|
283
|
+
greeting = self._read_packet()
|
|
284
|
+
if greeting and greeting[0] == 0xff:
|
|
285
|
+
self._server_error(greeting)
|
|
286
|
+
if len(greeting) < 34 or greeting[0] != 10:
|
|
287
|
+
raise OperationalError("MySQL: unsupported or malformed server greeting")
|
|
288
|
+
|
|
289
|
+
pos = greeting.index(b"\x00", 1) + 1
|
|
290
|
+
pos += 4
|
|
291
|
+
salt = greeting[pos:pos + 8]
|
|
292
|
+
pos += 9
|
|
293
|
+
lower = int.from_bytes(greeting[pos:pos + 2], "little")
|
|
294
|
+
pos += 2
|
|
295
|
+
if pos >= len(greeting):
|
|
296
|
+
raise OperationalError("MySQL: incomplete server greeting")
|
|
297
|
+
pos += 1 # character set
|
|
298
|
+
pos += 2 # status flags
|
|
299
|
+
upper = int.from_bytes(greeting[pos:pos + 2], "little")
|
|
300
|
+
pos += 2
|
|
301
|
+
server_caps = lower | (upper << 16)
|
|
302
|
+
|
|
303
|
+
auth_len = greeting[pos] if pos < len(greeting) else 0
|
|
304
|
+
pos += 1 + 10
|
|
305
|
+
if server_caps & CLIENT_SECURE_CONNECTION and pos < len(greeting):
|
|
306
|
+
second_len = max(13, auth_len - 8) if auth_len else 13
|
|
307
|
+
second_salt = greeting[pos:pos + second_len]
|
|
308
|
+
if second_salt.endswith(b"\x00"):
|
|
309
|
+
second_salt = second_salt[:-1]
|
|
310
|
+
salt += second_salt
|
|
311
|
+
pos += second_len
|
|
312
|
+
plugin = "mysql_native_password"
|
|
313
|
+
if server_caps & CLIENT_PLUGIN_AUTH and pos < len(greeting):
|
|
314
|
+
plugin_end = greeting.find(b"\x00", pos)
|
|
315
|
+
if plugin_end < 0:
|
|
316
|
+
plugin_end = len(greeting)
|
|
317
|
+
plugin = greeting[pos:plugin_end].decode("ascii")
|
|
318
|
+
requested = (CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_PROTOCOL_41 |
|
|
319
|
+
CLIENT_TRANSACTIONS | CLIENT_SECURE_CONNECTION |
|
|
320
|
+
CLIENT_PLUGIN_AUTH)
|
|
321
|
+
if database:
|
|
322
|
+
requested |= CLIENT_CONNECT_WITH_DB
|
|
323
|
+
if self._use_ssl:
|
|
324
|
+
requested |= CLIENT_SSL
|
|
325
|
+
if not server_caps & CLIENT_PROTOCOL_41:
|
|
326
|
+
raise OperationalError("MySQL: server does not support protocol 4.1")
|
|
327
|
+
if not server_caps & CLIENT_SECURE_CONNECTION:
|
|
328
|
+
raise OperationalError("MySQL: server does not support secure authentication")
|
|
329
|
+
if not server_caps & CLIENT_PLUGIN_AUTH:
|
|
330
|
+
raise OperationalError("MySQL: server does not support authentication plugins")
|
|
331
|
+
client_caps = requested & server_caps
|
|
332
|
+
if database and not client_caps & CLIENT_CONNECT_WITH_DB:
|
|
333
|
+
raise OperationalError("MySQL: server does not support selecting a database at login")
|
|
334
|
+
|
|
335
|
+
if self._use_ssl:
|
|
336
|
+
if not server_caps & CLIENT_SSL:
|
|
337
|
+
raise OperationalError("MySQL: server does not support TLS")
|
|
338
|
+
self._seq = 1
|
|
339
|
+
ssl_request = (client_caps.to_bytes(4, "little") +
|
|
340
|
+
(1024 * 1024).to_bytes(4, "little") +
|
|
341
|
+
b"\x2d" + bytes(23))
|
|
342
|
+
self._send_packet(ssl_request)
|
|
343
|
+
import ssl
|
|
344
|
+
try:
|
|
345
|
+
self.sock = ssl.wrap_socket(self.sock, server_hostname=self.host)
|
|
346
|
+
except TypeError:
|
|
347
|
+
self.sock = ssl.wrap_socket(self.sock)
|
|
348
|
+
self._tls_active = True
|
|
349
|
+
|
|
350
|
+
auth = _auth_response(plugin, password, salt, self._tls_active)
|
|
351
|
+
response = (client_caps.to_bytes(4, "little") +
|
|
352
|
+
(1024 * 1024).to_bytes(4, "little") +
|
|
353
|
+
b"\x2d" + bytes(23) + user.encode("utf-8") + b"\x00" +
|
|
354
|
+
bytes((len(auth),)) + auth)
|
|
355
|
+
if database:
|
|
356
|
+
response += database.encode("utf-8") + b"\x00"
|
|
357
|
+
response += plugin.encode("ascii") + b"\x00"
|
|
358
|
+
self._send_packet(response)
|
|
359
|
+
|
|
360
|
+
while True:
|
|
361
|
+
packet = self._read_packet()
|
|
362
|
+
if not packet:
|
|
363
|
+
raise OperationalError("MySQL: empty authentication response")
|
|
364
|
+
if packet[0] == 0x00:
|
|
365
|
+
return
|
|
366
|
+
if packet[0] == 0xff:
|
|
367
|
+
self._server_error(packet)
|
|
368
|
+
if packet[0] == 0xfe:
|
|
369
|
+
end = packet.find(b"\x00", 1)
|
|
370
|
+
if end < 0:
|
|
371
|
+
raise OperationalError("MySQL: malformed authentication switch")
|
|
372
|
+
plugin = packet[1:end].decode("ascii")
|
|
373
|
+
salt = packet[end + 1:]
|
|
374
|
+
if salt.endswith(b"\x00"):
|
|
375
|
+
salt = salt[:-1]
|
|
376
|
+
self._send_packet(
|
|
377
|
+
_auth_response(plugin, password, salt, self._tls_active)
|
|
378
|
+
)
|
|
379
|
+
continue
|
|
380
|
+
if packet[0] == 0x01 and len(packet) > 1:
|
|
381
|
+
if packet[1] == 0x03:
|
|
382
|
+
continue
|
|
383
|
+
if packet[1] == 0x04:
|
|
384
|
+
if not self._tls_active:
|
|
385
|
+
raise OperationalError(
|
|
386
|
+
"MySQL: caching_sha2_password full authentication requires use_ssl=True"
|
|
387
|
+
)
|
|
388
|
+
self._send_packet(password.encode("utf-8") + b"\x00")
|
|
389
|
+
continue
|
|
390
|
+
raise OperationalError("MySQL: unexpected authentication response")
|
|
391
|
+
|
|
392
|
+
def _server_error(self, packet):
|
|
393
|
+
code = int.from_bytes(packet[1:3], "little") if len(packet) >= 3 else 0
|
|
394
|
+
pos = 3
|
|
395
|
+
state = ""
|
|
396
|
+
if len(packet) >= 9 and packet[3:4] == b"#":
|
|
397
|
+
state = " " + packet[4:9].decode("ascii", "replace")
|
|
398
|
+
pos = 9
|
|
399
|
+
message = packet[pos:].decode(self.encoding, "replace")
|
|
400
|
+
raise OperationalError("MySQL error %d%s: %s" % (code, state, message))
|
|
401
|
+
|
|
402
|
+
def _read_ok(self, packet, cursor):
|
|
403
|
+
pos = 1
|
|
404
|
+
affected, pos = _lenenc_int(packet, pos)
|
|
405
|
+
last_id, pos = _lenenc_int(packet, pos)
|
|
406
|
+
if len(packet) >= pos + 4:
|
|
407
|
+
status = int.from_bytes(packet[pos:pos + 2], "little")
|
|
408
|
+
pos += 4 # status and warnings
|
|
409
|
+
else:
|
|
410
|
+
status = 0
|
|
411
|
+
if cursor:
|
|
412
|
+
cursor.description = None
|
|
413
|
+
cursor.rowcount = affected if affected is not None else 0
|
|
414
|
+
cursor.lastrowid = last_id if last_id is not None else 0
|
|
415
|
+
cursor._rows = []
|
|
416
|
+
return status
|
|
417
|
+
|
|
418
|
+
def _column_definition(self, packet):
|
|
419
|
+
pos = 0
|
|
420
|
+
fields = []
|
|
421
|
+
for _ in range(6):
|
|
422
|
+
value, pos = _lenenc_bytes(packet, pos)
|
|
423
|
+
fields.append(value)
|
|
424
|
+
fixed_size, pos = _lenenc_int(packet, pos)
|
|
425
|
+
if fixed_size is None or len(packet) < pos + fixed_size:
|
|
426
|
+
raise OperationalError("MySQL: malformed column definition")
|
|
427
|
+
charset = int.from_bytes(packet[pos:pos + 2], "little")
|
|
428
|
+
length = int.from_bytes(packet[pos + 2:pos + 6], "little")
|
|
429
|
+
column_type = packet[pos + 6]
|
|
430
|
+
flags = int.from_bytes(packet[pos + 7:pos + 9], "little")
|
|
431
|
+
name = fields[4].decode(self.encoding, "replace") if fields[4] else ""
|
|
432
|
+
return (name, column_type, charset, flags, length)
|
|
433
|
+
|
|
434
|
+
def _decode_value(self, value, column):
|
|
435
|
+
if value is None:
|
|
436
|
+
return None
|
|
437
|
+
column_type, charset, flags = column[1], column[2], column[3]
|
|
438
|
+
if column_type in (249, 250, 251, 252):
|
|
439
|
+
return value if charset == 63 else value.decode(self.encoding, "replace")
|
|
440
|
+
if column_type in (15, 253, 254) and (charset == 63 or flags & 0x80):
|
|
441
|
+
return value
|
|
442
|
+
text = value.decode(self.encoding, "replace")
|
|
443
|
+
if column_type in (1, 2, 3, 8, 9, 13):
|
|
444
|
+
try:
|
|
445
|
+
return int(text)
|
|
446
|
+
except ValueError:
|
|
447
|
+
return text
|
|
448
|
+
if column_type in (4, 5):
|
|
449
|
+
try:
|
|
450
|
+
return float(text)
|
|
451
|
+
except ValueError:
|
|
452
|
+
return text
|
|
453
|
+
return text
|
|
454
|
+
|
|
455
|
+
def _read_rows(self, columns, cursor):
|
|
456
|
+
rows = []
|
|
457
|
+
while True:
|
|
458
|
+
packet = self._read_packet()
|
|
459
|
+
if packet and packet[0] == 0xff:
|
|
460
|
+
self._server_error(packet)
|
|
461
|
+
if packet and packet[0] == 0xfe and len(packet) < 9:
|
|
462
|
+
break
|
|
463
|
+
if packet and packet[0] == 0xfb:
|
|
464
|
+
raise OperationalError("MySQL: LOCAL INFILE is not supported")
|
|
465
|
+
pos = 0
|
|
466
|
+
row = []
|
|
467
|
+
for column in columns:
|
|
468
|
+
value, pos = _lenenc_bytes(packet, pos)
|
|
469
|
+
row.append(self._decode_value(value, column))
|
|
470
|
+
rows.append(tuple(row))
|
|
471
|
+
if cursor:
|
|
472
|
+
cursor._rows = rows
|
|
473
|
+
cursor.description = [
|
|
474
|
+
(column[0], column[1], None, column[4], None, None, None)
|
|
475
|
+
for column in columns
|
|
476
|
+
]
|
|
477
|
+
cursor.rowcount = len(rows)
|
|
478
|
+
cursor.lastrowid = 0
|
|
479
|
+
|
|
480
|
+
def _run_query(self, query, cursor):
|
|
481
|
+
self._seq = 0
|
|
482
|
+
self._send_packet(b"\x03" + query.encode(self.encoding))
|
|
483
|
+
packet = self._read_packet()
|
|
484
|
+
if packet and packet[0] == 0xff:
|
|
485
|
+
self._server_error(packet)
|
|
486
|
+
if packet and packet[0] == 0x00:
|
|
487
|
+
self._read_ok(packet, cursor)
|
|
488
|
+
return
|
|
489
|
+
if packet and packet[0] == 0xfb:
|
|
490
|
+
raise OperationalError("MySQL: LOCAL INFILE is not supported")
|
|
491
|
+
column_count, _ = _lenenc_int(packet)
|
|
492
|
+
if column_count is None:
|
|
493
|
+
raise OperationalError("MySQL: malformed result set")
|
|
494
|
+
columns = []
|
|
495
|
+
for _ in range(column_count):
|
|
496
|
+
definition = self._read_packet()
|
|
497
|
+
if definition and definition[0] == 0xff:
|
|
498
|
+
self._server_error(definition)
|
|
499
|
+
columns.append(self._column_definition(definition))
|
|
500
|
+
terminator = self._read_packet()
|
|
501
|
+
if terminator and terminator[0] == 0xff:
|
|
502
|
+
self._server_error(terminator)
|
|
503
|
+
self._read_rows(columns, cursor)
|
|
504
|
+
|
|
505
|
+
def cursor(self):
|
|
506
|
+
return Cursor(self)
|
|
507
|
+
|
|
508
|
+
def execute(self, query, obj=None):
|
|
509
|
+
if not self.sock:
|
|
510
|
+
_lost()
|
|
511
|
+
if not isinstance(query, str):
|
|
512
|
+
raise ProgrammingError("MySQL query must be a string")
|
|
513
|
+
if not self.autocommit and not self._in_transaction:
|
|
514
|
+
self._run_query("START TRANSACTION", None)
|
|
515
|
+
self._in_transaction = True
|
|
516
|
+
self._run_query(query, obj)
|
|
517
|
+
|
|
518
|
+
def begin(self):
|
|
519
|
+
if self._in_transaction:
|
|
520
|
+
self.rollback()
|
|
521
|
+
self._run_query("START TRANSACTION", None)
|
|
522
|
+
self._in_transaction = True
|
|
523
|
+
|
|
524
|
+
def commit(self):
|
|
525
|
+
if self.sock and self._in_transaction:
|
|
526
|
+
self._run_query("COMMIT", None)
|
|
527
|
+
self._in_transaction = False
|
|
528
|
+
|
|
529
|
+
def rollback(self):
|
|
530
|
+
if self.sock and self._in_transaction:
|
|
531
|
+
self._run_query("ROLLBACK", None)
|
|
532
|
+
self._in_transaction = False
|
|
533
|
+
|
|
534
|
+
def close(self):
|
|
535
|
+
sock = self.sock
|
|
536
|
+
if sock:
|
|
537
|
+
try:
|
|
538
|
+
self._seq = 0
|
|
539
|
+
self._send_packet(b"\x01")
|
|
540
|
+
finally:
|
|
541
|
+
sock.close()
|
|
542
|
+
self.sock = None
|
|
543
|
+
self._in_transaction = False
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _quote_identifier(identifier):
|
|
547
|
+
if not isinstance(identifier, str) or not identifier:
|
|
548
|
+
raise ProgrammingError("database name must be a non-empty string")
|
|
549
|
+
return "`" + identifier.replace("`", "``") + "`"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def create_database(host, user, password, database, port=3306, use_ssl=True):
|
|
553
|
+
connection = connect(host, user, password, None, port, use_ssl)
|
|
554
|
+
try:
|
|
555
|
+
connection.autocommit = True
|
|
556
|
+
connection.execute("CREATE DATABASE " + _quote_identifier(database))
|
|
557
|
+
finally:
|
|
558
|
+
connection.close()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def drop_database(host, user, password, database, port=3306, use_ssl=True):
|
|
562
|
+
connection = connect(host, user, password, None, port, use_ssl)
|
|
563
|
+
try:
|
|
564
|
+
connection.autocommit = True
|
|
565
|
+
connection.execute("DROP DATABASE " + _quote_identifier(database))
|
|
566
|
+
finally:
|
|
567
|
+
connection.close()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ownSQL
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.
|
|
5
|
+
Home-page: https://github.com/TimonW-Dev/ownSQL
|
|
6
|
+
Author: TimonW-Dev
|
|
7
|
+
Author-email: timon.github@outlook.com
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/TimonW-Dev/ownSQL/issues
|
|
9
|
+
Project-URL: Source Code, https://github.com/TimonW-Dev/ownSQL
|
|
10
|
+
Keywords: micropython mysql esp8266 esp32 database driver
|
|
11
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: Other OS
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Requires-Python: >=3.4
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: keywords
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
Dynamic: project-url
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# ownSQL
|
|
33
|
+
|
|
34
|
+
**A MySQL database driver for MicroPython, supporting MySQL Server 8.x and 9.x.**
|
|
35
|
+
|
|
36
|
+
ownSQL was derived from `micropg_lite` and adapted from PostgreSQL to MySQL with the assistance of AI tools. `micropg`, the original project on which `micropg_lite` is based, was developed by [Hajime Nakagami](https://github.com/nakagami). Many thanks for the great work — without `micropg`, `ownSQL` would not exist!
|
|
37
|
+
|
|
38
|
+
For support, questions, or suggestions, please [open an issue](https://github.com/TimonW-Dev/ownSQL/issues) or contact me by email using the address linked in [my profile](https://github.com/TimonW-Dev). Pull requests are welcome. This is a voluntary open-source project, so support is provided as time allows.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## 📥 Installation
|
|
42
|
+
|
|
43
|
+
### Choose a file
|
|
44
|
+
|
|
45
|
+
We recommend [`ownSQL.py`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py). If you want to save RAM, you can use the precompiled [`ownSQL.mpy`](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.mpy) file. For most projects, the `.py` file is sufficient. The `.mpy` file may not be compatible with every MicroPython firmware version and is not as easy to edit or debug.
|
|
46
|
+
|
|
47
|
+
### Install on your microcontroller
|
|
48
|
+
|
|
49
|
+
1. Download the [ownSQL.py](https://github.com/TimonW-Dev/ownSQL/blob/main/ownSQL.py) file from this [repository](https://github.com/TimonW-Dev/ownSQL) to your local computer.
|
|
50
|
+
2. Copy it to the `/lib` folder on your microcontroller using Thonny IDE or another file transfer tool. Create the folder if it does not exist.
|
|
51
|
+
- **Thonny tip:** Open **View** in the top bar and enable **Files** to upload files and create folders on the microcontroller.
|
|
52
|
+
3. Import the library in your MicroPython script. The import name is the same for both file formats:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import ownSQL
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The files on your microcontroller should be arranged like this:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/
|
|
62
|
+
|- example.py
|
|
63
|
+
|- lib/
|
|
64
|
+
|- ownSQL.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 🚀 Examples
|
|
68
|
+
|
|
69
|
+
Establish a network connection before running `ownSQL` code. The SELECT example below includes a Wi-Fi connection template; the other examples assume that the connection is already established.
|
|
70
|
+
|
|
71
|
+
The [`examples/` folder](https://github.com/TimonW-Dev/ownSQL/tree/main/examples) contains:
|
|
72
|
+
|
|
73
|
+
- [`docker-full-testing-container-setup`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/docker-full-testing-container-setup), to create an empty MySQL container.
|
|
74
|
+
- [`exampleDatabase.sql`](https://github.com/TimonW-Dev/ownSQL/blob/main/examples/exampleDatabase.sql), the database used by the examples.
|
|
75
|
+
- Complete test scripts with WLAN connection templates, plus examples for creating and dropping tables and databases.
|
|
76
|
+
|
|
77
|
+
### 🔍 SELECT (including Wi-Fi setup)
|
|
78
|
+
|
|
79
|
+
Replace the Wi-Fi credentials and server details with your own:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import ownSQL
|
|
83
|
+
import network
|
|
84
|
+
import time
|
|
85
|
+
|
|
86
|
+
ssid = 'wifissid'
|
|
87
|
+
password = 'secret'
|
|
88
|
+
|
|
89
|
+
# Connect to the network
|
|
90
|
+
wlan = network.WLAN(network.STA_IF)
|
|
91
|
+
wlan.active(True)
|
|
92
|
+
wlan.connect(ssid, password)
|
|
93
|
+
|
|
94
|
+
while not wlan.isconnected():
|
|
95
|
+
print("Wi-Fi status:", wlan.status())
|
|
96
|
+
time.sleep(1)
|
|
97
|
+
|
|
98
|
+
print("Wi-Fi connected")
|
|
99
|
+
|
|
100
|
+
conn = ownSQL.connect(host='127.0.0.1', # Replace with your server's IP address
|
|
101
|
+
user='root',
|
|
102
|
+
password='123456',
|
|
103
|
+
database='exampledatabase')
|
|
104
|
+
cur = conn.cursor()
|
|
105
|
+
|
|
106
|
+
cur.execute('select * from customers')
|
|
107
|
+
selectresult = cur.fetchall()
|
|
108
|
+
conn.close()
|
|
109
|
+
|
|
110
|
+
# Print the raw data
|
|
111
|
+
print(selectresult)
|
|
112
|
+
|
|
113
|
+
# Print the data as rows
|
|
114
|
+
for row in selectresult:
|
|
115
|
+
for value in row:
|
|
116
|
+
print(value, end="\t")
|
|
117
|
+
print()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### ➕ INSERT
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
conn = ownSQL.connect(
|
|
124
|
+
host='127.0.0.1', # Replace with your server's IP address
|
|
125
|
+
user='root',
|
|
126
|
+
password='123456',
|
|
127
|
+
database='exampledatabase'
|
|
128
|
+
)
|
|
129
|
+
cur = conn.cursor()
|
|
130
|
+
|
|
131
|
+
cur.execute(
|
|
132
|
+
'INSERT INTO customers (id, firstName, lastName, email) values (%s, %s, %s, %s)',
|
|
133
|
+
['5', 'David', 'Wilson', 'david.wilson@example.com']
|
|
134
|
+
)
|
|
135
|
+
conn.commit()
|
|
136
|
+
conn.close()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### ✏️ UPDATE
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
conn = ownSQL.connect(
|
|
143
|
+
host='127.0.0.1', # Replace with your server's IP address
|
|
144
|
+
user='root',
|
|
145
|
+
password='123456',
|
|
146
|
+
database='exampledatabase'
|
|
147
|
+
)
|
|
148
|
+
cur = conn.cursor()
|
|
149
|
+
|
|
150
|
+
cur.execute("update customers set firstName='UpdatedFirstName' where id=2;")
|
|
151
|
+
conn.commit()
|
|
152
|
+
conn.close()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 🗑️ DELETE
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
conn = ownSQL.connect(
|
|
159
|
+
host='127.0.0.1', # Replace with your server's IP address
|
|
160
|
+
user='root',
|
|
161
|
+
password='123456',
|
|
162
|
+
database='exampledatabase'
|
|
163
|
+
)
|
|
164
|
+
cur = conn.cursor()
|
|
165
|
+
|
|
166
|
+
cur.execute("delete from customers where id=1;")
|
|
167
|
+
conn.commit()
|
|
168
|
+
conn.close()
|
|
169
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ownSQL.py,sha256=OaKVNDWrnaulP-c1OBBwcy6j69Mp8Bxp6seGM0fI9tw,21000
|
|
2
|
+
ownsql-1.0.0.dist-info/licenses/LICENSE,sha256=aNITQfVMbd7OQZgsiwiRL7T5wS36pAfliHsc744gE8c,1115
|
|
3
|
+
ownsql-1.0.0.dist-info/METADATA,sha256=Pqbv-5s3PjQDQKFjzrGjmNDiTRAiSGthEGLSRuOuwHA,5581
|
|
4
|
+
ownsql-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
ownsql-1.0.0.dist-info/top_level.txt,sha256=Z7NtFAE9UzF1YkpCmWfPJuliMpuAnHWk9pNIheHP8GA,7
|
|
6
|
+
ownsql-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timon W
|
|
4
|
+
Copyright (c) 2014-2019, 2021-2026 Hajime Nakagami
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ownSQL
|