mysqlx-connector-python 8.3.0__py2.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.
mysqlx/protocol.py ADDED
@@ -0,0 +1,1214 @@
1
+ # Copyright (c) 2016, 2023, Oracle and/or its affiliates.
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License, version 2.0, as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is also distributed with certain software (including
8
+ # but not limited to OpenSSL) that is licensed under separate terms,
9
+ # as designated in a particular file or component or in included license
10
+ # documentation. The authors of MySQL hereby grant you an
11
+ # additional permission to link the program and your derivative works
12
+ # with the separately licensed software that they have included with
13
+ # MySQL.
14
+ #
15
+ # Without limiting anything contained in the foregoing, this file,
16
+ # which is part of MySQL Connector/Python, is also subject to the
17
+ # Universal FOSS Exception, version 1.0, a copy of which can be found at
18
+ # http://oss.oracle.com/licenses/universal-foss-exception.
19
+ #
20
+ # This program is distributed in the hope that it will be useful, but
21
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
23
+ # See the GNU General Public License, version 2.0, for more details.
24
+ #
25
+ # You should have received a copy of the GNU General Public License
26
+ # along with this program; if not, write to the Free Software Foundation, Inc.,
27
+ # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
28
+
29
+ """Implementation of the X protocol for MySQL servers."""
30
+
31
+ import struct
32
+ import zlib
33
+
34
+ from io import BytesIO
35
+ from typing import Any, Dict, List, Optional, Tuple, Union
36
+
37
+ try:
38
+ import lz4.frame
39
+
40
+ HAVE_LZ4 = True
41
+ except ImportError:
42
+ HAVE_LZ4 = False
43
+
44
+ try:
45
+ import zstandard as zstd
46
+
47
+ HAVE_ZSTD = True
48
+ except ImportError:
49
+ HAVE_ZSTD = False
50
+
51
+ from .errors import (
52
+ InterfaceError,
53
+ NotSupportedError,
54
+ OperationalError,
55
+ ProgrammingError,
56
+ )
57
+ from .expr import (
58
+ ExprParser,
59
+ build_bool_scalar,
60
+ build_expr,
61
+ build_int_scalar,
62
+ build_scalar,
63
+ build_unsigned_int_scalar,
64
+ )
65
+ from .helpers import encode_to_bytes, get_item_or_attr
66
+ from .logger import logger
67
+ from .protobuf import CRUD_PREPARE_MAPPING, SERVER_MESSAGES, Message, mysqlxpb_enum
68
+ from .result import Column
69
+ from .statement import (
70
+ AddStatement,
71
+ DeleteStatement,
72
+ FilterableStatement,
73
+ FindStatement,
74
+ InsertStatement,
75
+ ModifyStatement,
76
+ ReadStatement,
77
+ RemoveStatement,
78
+ SqlStatement,
79
+ UpdateStatement,
80
+ )
81
+ from .types import (
82
+ ColumnType,
83
+ MessageType,
84
+ ProtobufMessageCextType,
85
+ ProtobufMessageType,
86
+ ResultBaseType,
87
+ SocketType,
88
+ StatementType,
89
+ StrOrBytes,
90
+ )
91
+
92
+ _COMPRESSION_THRESHOLD = 1000
93
+
94
+
95
+ class Compressor:
96
+ """Implements compression/decompression using `zstd_stream`, `lz4_message`
97
+ and `deflate_stream` algorithms.
98
+
99
+ Args:
100
+ algorithm (str): Compression algorithm.
101
+
102
+ .. versionadded:: 8.0.21
103
+
104
+ """
105
+
106
+ def __init__(self, algorithm: str) -> None:
107
+ self._algorithm: str = algorithm
108
+ self._compressobj: Any = None
109
+ self._decompressobj: Any = None
110
+
111
+ if algorithm == "zstd_stream":
112
+ self._compressobj = zstd.ZstdCompressor()
113
+ self._decompressobj = zstd.ZstdDecompressor()
114
+ elif algorithm == "deflate_stream":
115
+ self._compressobj = zlib.compressobj()
116
+ self._decompressobj = zlib.decompressobj()
117
+
118
+ def compress(self, data: StrOrBytes) -> bytes:
119
+ """Compresses data and returns it.
120
+
121
+ Args:
122
+ data (str, bytes or buffer object): Data to be compressed.
123
+
124
+ Returns:
125
+ bytes: Compressed data.
126
+ """
127
+ if self._algorithm == "zstd_stream":
128
+ return self._compressobj.compress(data)
129
+ if self._algorithm == "lz4_message":
130
+ with lz4.frame.LZ4FrameCompressor() as compressor:
131
+ compressed = compressor.begin()
132
+ compressed += compressor.compress(data)
133
+ compressed += compressor.flush()
134
+ return compressed
135
+
136
+ # Using 'deflate_stream' algorithm
137
+ compressed = self._compressobj.compress(data)
138
+ compressed += self._compressobj.flush(zlib.Z_SYNC_FLUSH)
139
+ return compressed
140
+
141
+ def decompress(self, data: StrOrBytes) -> bytes:
142
+ """Decompresses a frame of data and returns it as a string of bytes.
143
+
144
+ Args:
145
+ data (str, bytes or buffer object): Data to be compressed.
146
+
147
+ Returns:
148
+ bytes: Decompresssed data.
149
+ """
150
+ if self._algorithm == "zstd_stream":
151
+ return self._decompressobj.decompress(data)
152
+ if self._algorithm == "lz4_message":
153
+ with lz4.frame.LZ4FrameDecompressor() as decompressor:
154
+ decompressed = decompressor.decompress(data)
155
+ return decompressed
156
+
157
+ # Using 'deflate' algorithm
158
+ decompressed = self._decompressobj.decompress(data)
159
+ decompressed += self._decompressobj.flush(zlib.Z_SYNC_FLUSH)
160
+ return decompressed
161
+
162
+
163
+ class MessageReader:
164
+ """Implements a Message Reader.
165
+
166
+ Args:
167
+ socket_stream (mysqlx.connection.SocketStream): `SocketStream` object.
168
+
169
+ .. versionadded:: 8.0.21
170
+ """
171
+
172
+ def __init__(self, socket_stream: SocketType) -> None:
173
+ self._stream: SocketType = socket_stream
174
+ self._compressor: Optional[Compressor] = None
175
+ self._msg: MessageType = None
176
+ self._msg_queue: List[Message] = []
177
+
178
+ def _read_message(self) -> MessageType:
179
+ """Reads X Protocol messages from the stream and returns a
180
+ :class:`mysqlx.protobuf.Message` object.
181
+
182
+ Raises:
183
+ :class:`mysqlx.ProgrammingError`: If e connected server does not
184
+ have the MySQL X protocol plugin
185
+ enabled.
186
+
187
+ Returns:
188
+ mysqlx.protobuf.Message: MySQL X Protobuf Message.
189
+ """
190
+ if self._msg_queue:
191
+ return self._msg_queue.pop(0)
192
+
193
+ frame_size, frame_type = struct.unpack("<LB", self._stream.read(5))
194
+
195
+ if frame_type == 10:
196
+ raise ProgrammingError(
197
+ "The connected server does not have the "
198
+ "MySQL X protocol plugin enabled or "
199
+ "protocol mismatch"
200
+ )
201
+
202
+ frame_payload = self._stream.read(frame_size - 1)
203
+ if frame_type not in SERVER_MESSAGES:
204
+ raise ValueError(f"Unknown message type: {frame_type}")
205
+
206
+ # Do not parse empty notices, Message requires a type in payload
207
+ if frame_type == 11 and frame_payload == b"":
208
+ return self._read_message()
209
+
210
+ frame_msg = Message.from_server_message(frame_type, frame_payload)
211
+
212
+ if frame_type == 19: # Mysqlx.ServerMessages.Type.COMPRESSION
213
+ uncompressed_size = frame_msg["uncompressed_size"]
214
+ stream = BytesIO(self._compressor.decompress(frame_msg["payload"]))
215
+ bytes_processed = 0
216
+ while bytes_processed < uncompressed_size:
217
+ payload_size, msg_type = struct.unpack("<LB", stream.read(5))
218
+ payload = stream.read(payload_size - 1)
219
+ self._msg_queue.append(Message.from_server_message(msg_type, payload))
220
+ bytes_processed += payload_size + 4
221
+ return self._msg_queue.pop(0) if self._msg_queue else None
222
+
223
+ return frame_msg
224
+
225
+ def read_message(self) -> MessageType:
226
+ """Read message.
227
+
228
+ Returns:
229
+ mysqlx.protobuf.Message: MySQL X Protobuf Message.
230
+ """
231
+ if self._msg is not None:
232
+ msg = self._msg
233
+ self._msg = None
234
+ return msg
235
+ return self._read_message()
236
+
237
+ def push_message(self, msg: MessageType) -> None:
238
+ """Push message.
239
+
240
+ Args:
241
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
242
+
243
+ Raises:
244
+ :class:`mysqlx.OperationalError`: If message push slot is full.
245
+ """
246
+ if self._msg is not None:
247
+ raise OperationalError("Message push slot is full")
248
+ self._msg = msg
249
+
250
+ def set_compression(self, algorithm: str) -> None:
251
+ """Creates a :class:`mysqlx.protocol.Compressor` object based on the
252
+ compression algorithm.
253
+
254
+ Args:
255
+ algorithm (str): Compression algorithm.
256
+
257
+ .. versionadded:: 8.0.21
258
+
259
+ """
260
+ self._compressor = Compressor(algorithm) if algorithm else None
261
+
262
+
263
+ class MessageWriter:
264
+ """Implements a Message Writer.
265
+
266
+ Args:
267
+ socket_stream (mysqlx.connection.SocketStream): `SocketStream` object.
268
+
269
+ .. versionadded:: 8.0.21
270
+
271
+ """
272
+
273
+ def __init__(self, socket_stream: SocketType) -> None:
274
+ self._stream: SocketType = socket_stream
275
+ self._compressor: Optional[Compressor] = None
276
+
277
+ def write_message(self, msg_type: int, msg: MessageType) -> None:
278
+ """Write message.
279
+
280
+ Args:
281
+ msg_type (int): The message type.
282
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
283
+ """
284
+ msg_size = msg.byte_size(msg)
285
+ if self._compressor and msg_size > _COMPRESSION_THRESHOLD:
286
+ msg_str = encode_to_bytes(msg.serialize_to_string())
287
+ header = struct.pack("<LB", msg_size + 1, msg_type)
288
+ compressed = self._compressor.compress(b"".join([header, msg_str]))
289
+
290
+ msg_first_fields = Message("Mysqlx.Connection.Compression")
291
+ msg_first_fields["client_messages"] = msg_type
292
+ msg_first_fields["uncompressed_size"] = msg_size + 5
293
+
294
+ msg_payload = Message("Mysqlx.Connection.Compression")
295
+ msg_payload["payload"] = compressed
296
+
297
+ output = b"".join(
298
+ [
299
+ encode_to_bytes(msg_first_fields.serialize_partial_to_string())[
300
+ :-2
301
+ ],
302
+ encode_to_bytes(msg_payload.serialize_partial_to_string()),
303
+ ]
304
+ )
305
+
306
+ msg_comp_id = mysqlxpb_enum("Mysqlx.ClientMessages.Type.COMPRESSION")
307
+ header = struct.pack("<LB", len(output) + 1, msg_comp_id)
308
+ self._stream.sendall(b"".join([header, output]))
309
+ else:
310
+ msg_str = encode_to_bytes(msg.serialize_to_string())
311
+ header = struct.pack("<LB", msg_size + 1, msg_type)
312
+ self._stream.sendall(b"".join([header, msg_str]))
313
+
314
+ def set_compression(self, algorithm: str) -> None:
315
+ """Creates a :class:`mysqlx.protocol.Compressor` object based on the
316
+ compression algorithm.
317
+
318
+ Args:
319
+ algorithm (str): Compression algorithm.
320
+ """
321
+ self._compressor = Compressor(algorithm) if algorithm else None
322
+
323
+
324
+ class Protocol:
325
+ """Implements the MySQL X Protocol.
326
+
327
+ Args:
328
+ read (mysqlx.protocol.MessageReader): A Message Reader object.
329
+ writer (mysqlx.protocol.MessageWriter): A Message Writer object.
330
+
331
+ .. versionchanged:: 8.0.21
332
+ """
333
+
334
+ def __init__(self, reader: MessageReader, writer: MessageWriter) -> None:
335
+ self._reader: MessageReader = reader
336
+ self._writer: MessageWriter = writer
337
+ self._compression_algorithm: Optional[str] = None
338
+ self._warnings: List[str] = []
339
+
340
+ @property
341
+ def compression_algorithm(self) -> Optional[str]:
342
+ """str: The compresion algorithm."""
343
+ return self._compression_algorithm
344
+
345
+ @staticmethod
346
+ def _apply_filter(msg: MessageType, stmt: FilterableStatement) -> None:
347
+ """Apply filter.
348
+
349
+ Args:
350
+ msg (mysqlx.protobuf.Message): The MySQL X Protobuf Message.
351
+ stmt (Statement): A `Statement` based type object.
352
+ """
353
+ if stmt.has_where:
354
+ msg["criteria"] = stmt.get_where_expr()
355
+ if stmt.has_sort:
356
+ msg["order"].extend(stmt.get_sort_expr())
357
+ if stmt.has_group_by:
358
+ msg["grouping"].extend(stmt.get_grouping())
359
+ if stmt.has_having:
360
+ msg["grouping_criteria"] = stmt.get_having()
361
+
362
+ def _create_any(self, arg: Any) -> Optional[MessageType]:
363
+ """Create any.
364
+
365
+ Args:
366
+ arg (object): Arbitrary object.
367
+
368
+ Returns:
369
+ mysqlx.protobuf.Message: MySQL X Protobuf Message.
370
+ """
371
+ if isinstance(arg, str):
372
+ value = Message("Mysqlx.Datatypes.Scalar.String", value=arg)
373
+ scalar = Message("Mysqlx.Datatypes.Scalar", type=8, v_string=value)
374
+ return Message("Mysqlx.Datatypes.Any", type=1, scalar=scalar)
375
+ if isinstance(arg, bool):
376
+ return Message(
377
+ "Mysqlx.Datatypes.Any", type=1, scalar=build_bool_scalar(arg)
378
+ )
379
+ if isinstance(arg, int):
380
+ if arg < 0:
381
+ return Message(
382
+ "Mysqlx.Datatypes.Any",
383
+ type=1,
384
+ scalar=build_int_scalar(arg),
385
+ )
386
+ return Message(
387
+ "Mysqlx.Datatypes.Any",
388
+ type=1,
389
+ scalar=build_unsigned_int_scalar(arg),
390
+ )
391
+ if isinstance(arg, tuple) and len(arg) == 2:
392
+ arg_key, arg_value = arg
393
+ obj_fld = Message(
394
+ "Mysqlx.Datatypes.Object.ObjectField",
395
+ key=arg_key,
396
+ value=self._create_any(arg_value),
397
+ )
398
+ obj = Message("Mysqlx.Datatypes.Object", fld=[obj_fld.get_message()])
399
+ return Message("Mysqlx.Datatypes.Any", type=2, obj=obj)
400
+ if isinstance(arg, dict) or (
401
+ isinstance(arg, (list, tuple)) and isinstance(arg[0], dict)
402
+ ):
403
+ array_values = []
404
+ for items in arg:
405
+ obj_flds = []
406
+ for key, value in items.items():
407
+ # Array can only handle Any types, Mysqlx.Datatypes.Any.obj
408
+ obj_fld = Message(
409
+ "Mysqlx.Datatypes.Object.ObjectField",
410
+ key=key,
411
+ value=self._create_any(value),
412
+ )
413
+ obj_flds.append(obj_fld.get_message())
414
+ msg_obj = Message("Mysqlx.Datatypes.Object", fld=obj_flds)
415
+ msg_any = Message("Mysqlx.Datatypes.Any", type=2, obj=msg_obj)
416
+ array_values.append(msg_any.get_message())
417
+
418
+ msg = Message("Mysqlx.Datatypes.Array")
419
+ msg["value"] = array_values
420
+ return Message("Mysqlx.Datatypes.Any", type=3, array=msg)
421
+ if isinstance(arg, list):
422
+ obj_flds = []
423
+ for key, value in arg:
424
+ obj_fld = Message(
425
+ "Mysqlx.Datatypes.Object.ObjectField",
426
+ key=key,
427
+ value=self._create_any(value),
428
+ )
429
+ obj_flds.append(obj_fld.get_message())
430
+ msg_obj = Message("Mysqlx.Datatypes.Object", fld=obj_flds)
431
+ msg_any = Message("Mysqlx.Datatypes.Any", type=2, obj=msg_obj)
432
+ return msg_any
433
+
434
+ return None
435
+
436
+ def _get_binding_args(
437
+ self, stmt: Union[FilterableStatement, SqlStatement], is_scalar: bool = True
438
+ ) -> Union[List[None], List[Union[ProtobufMessageType, ProtobufMessageCextType]]]:
439
+ """Returns the binding any/scalar.
440
+
441
+ Args:
442
+ stmt (Statement): A `Statement` based type object.
443
+ is_scalar (bool): `True` to return scalar values.
444
+
445
+ Raises:
446
+ :class:`mysqlx.ProgrammingError`: If unable to find placeholder for
447
+ parameter.
448
+
449
+ Returns:
450
+ list: A list of ``Any`` or ``Scalar`` objects.
451
+ """
452
+
453
+ def build_value(
454
+ value: Any,
455
+ ) -> Union[ProtobufMessageType, ProtobufMessageCextType]:
456
+ if is_scalar:
457
+ return build_scalar(value).get_message()
458
+ return self._create_any(value).get_message()
459
+
460
+ bindings = stmt.get_bindings()
461
+ binding_map = stmt.get_binding_map()
462
+
463
+ # If binding_map is None it's a SqlStatement object
464
+ if binding_map is None:
465
+ return [build_value(value) for value in bindings]
466
+
467
+ count = len(binding_map)
468
+ args: List[Any] = count * [None]
469
+ if count != len(bindings):
470
+ raise ProgrammingError(
471
+ "The number of bind parameters and placeholders do not match"
472
+ )
473
+ for name, value in bindings.items(): # type: ignore[union-attr]
474
+ if name not in binding_map:
475
+ raise ProgrammingError(
476
+ f"Unable to find placeholder for parameter: {name}"
477
+ )
478
+ pos = binding_map[name]
479
+ args[pos] = build_value(value)
480
+ return args
481
+
482
+ def _process_frame(self, msg: MessageType, result: ResultBaseType) -> None:
483
+ """Process frame.
484
+
485
+ Args:
486
+ msg (mysqlx.protobuf.Message): A MySQL X Protobuf Message.
487
+ result (Result): A `Result` based type object.
488
+ """
489
+ if msg["type"] == 1:
490
+ warn_msg = Message.from_message("Mysqlx.Notice.Warning", msg["payload"])
491
+ self._warnings.append(warn_msg.msg)
492
+ logger.warning(
493
+ "Protocol.process_frame Received Warning Notice code %s: %s",
494
+ warn_msg.code,
495
+ warn_msg.msg,
496
+ )
497
+ result.append_warning(warn_msg.level, warn_msg.code, warn_msg.msg)
498
+ elif msg["type"] == 2:
499
+ Message.from_message("Mysqlx.Notice.SessionVariableChanged", msg["payload"])
500
+ elif msg["type"] == 3:
501
+ sess_state_msg = Message.from_message(
502
+ "Mysqlx.Notice.SessionStateChanged", msg["payload"]
503
+ )
504
+ if sess_state_msg["param"] == mysqlxpb_enum(
505
+ "Mysqlx.Notice.SessionStateChanged.Parameter.GENERATED_DOCUMENT_IDS"
506
+ ):
507
+ result.set_generated_ids(
508
+ [
509
+ get_item_or_attr(
510
+ get_item_or_attr(value, "v_octets"), "value"
511
+ ).decode()
512
+ for value in sess_state_msg["value"]
513
+ ]
514
+ )
515
+ else: # Following results are unitary and not a list
516
+ sess_state_value = sess_state_msg["value"].pop()
517
+ if sess_state_msg["param"] == mysqlxpb_enum(
518
+ "Mysqlx.Notice.SessionStateChanged.Parameter.ROWS_AFFECTED"
519
+ ):
520
+ result.set_rows_affected(
521
+ get_item_or_attr(sess_state_value, "v_unsigned_int")
522
+ )
523
+ elif sess_state_msg["param"] == mysqlxpb_enum(
524
+ "Mysqlx.Notice.SessionStateChanged.Parameter.GENERATED_INSERT_ID"
525
+ ):
526
+ result.set_generated_insert_id(
527
+ get_item_or_attr(sess_state_value, "v_unsigned_int")
528
+ )
529
+
530
+ def _read_message(self, result: ResultBaseType) -> Optional[MessageType]:
531
+ """Read message.
532
+
533
+ Args:
534
+ result (Result): A `Result` based type object.
535
+ """
536
+ while True:
537
+ try:
538
+ msg = self._reader.read_message()
539
+ except RuntimeError as err:
540
+ warnings = repr(result.get_warnings())
541
+ if warnings:
542
+ raise RuntimeError(f"{err} reason: {warnings}") from err
543
+ if msg.type == "Mysqlx.Error":
544
+ raise OperationalError(msg["msg"], msg["code"])
545
+ if msg.type == "Mysqlx.Notice.Frame":
546
+ try:
547
+ self._process_frame(msg, result)
548
+ except (AttributeError, KeyError):
549
+ continue
550
+ elif msg.type == "Mysqlx.Sql.StmtExecuteOk":
551
+ return None
552
+ elif msg.type == "Mysqlx.Resultset.FetchDone":
553
+ result.set_closed(True)
554
+ elif msg.type == "Mysqlx.Resultset.FetchDoneMoreResultsets":
555
+ result.set_has_more_results(True)
556
+ elif msg.type == "Mysqlx.Resultset.Row":
557
+ result.set_has_data(True)
558
+ break
559
+ else:
560
+ break
561
+ return msg
562
+
563
+ def set_compression(self, algorithm: str) -> None:
564
+ """Sets the compression algorithm to be used by the compression
565
+ object, for uplink and downlink.
566
+
567
+ Args:
568
+ algorithm (str): Algorithm to be used in compression/decompression.
569
+
570
+ .. versionadded:: 8.0.21
571
+
572
+ """
573
+ self._compression_algorithm = algorithm
574
+ self._reader.set_compression(algorithm)
575
+ self._writer.set_compression(algorithm)
576
+
577
+ def get_capabilites(self) -> MessageType:
578
+ """Get capabilities.
579
+
580
+ Returns:
581
+ mysqlx.protobuf.Message: MySQL X Protobuf Message.
582
+ """
583
+ msg = Message("Mysqlx.Connection.CapabilitiesGet")
584
+ self._writer.write_message(
585
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_GET"),
586
+ msg,
587
+ )
588
+ msg = self._reader.read_message()
589
+ while msg.type == "Mysqlx.Notice.Frame":
590
+ msg = self._reader.read_message()
591
+
592
+ if msg.type == "Mysqlx.Error":
593
+ raise OperationalError(msg["msg"], msg["code"])
594
+
595
+ return msg
596
+
597
+ def set_capabilities(self, **kwargs: Any) -> None:
598
+ """Set capabilities.
599
+
600
+ Args:
601
+ **kwargs: Arbitrary keyword arguments.
602
+
603
+ Returns:
604
+ mysqlx.protobuf.Message: MySQL X Protobuf Message.
605
+ """
606
+ if not kwargs:
607
+ return None
608
+ capabilities = Message("Mysqlx.Connection.Capabilities")
609
+ for key, value in kwargs.items():
610
+ capability = Message("Mysqlx.Connection.Capability")
611
+ capability["name"] = key
612
+ if isinstance(value, dict):
613
+ items = value
614
+ obj_flds = []
615
+ for item in items:
616
+ obj_fld = Message(
617
+ "Mysqlx.Datatypes.Object.ObjectField",
618
+ key=item,
619
+ value=self._create_any(items[item]),
620
+ )
621
+ obj_flds.append(obj_fld.get_message())
622
+ msg_obj = Message("Mysqlx.Datatypes.Object", fld=obj_flds)
623
+ msg_any = Message("Mysqlx.Datatypes.Any", type=2, obj=msg_obj)
624
+ capability["value"] = msg_any.get_message()
625
+ else:
626
+ capability["value"] = self._create_any(value)
627
+
628
+ capabilities["capabilities"].extend([capability.get_message()])
629
+ msg = Message("Mysqlx.Connection.CapabilitiesSet")
630
+ msg["capabilities"] = capabilities
631
+ self._writer.write_message(
632
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CAPABILITIES_SET"),
633
+ msg,
634
+ )
635
+
636
+ try:
637
+ return self.read_ok()
638
+ except InterfaceError as err:
639
+ # Skip capability "session_connect_attrs" error since
640
+ # is only available on version >= 8.0.16
641
+ if err.errno != 5002:
642
+ raise
643
+ return None
644
+
645
+ def send_auth_start(
646
+ self,
647
+ method: str,
648
+ auth_data: Optional[str] = None,
649
+ initial_response: Optional[str] = None,
650
+ ) -> None:
651
+ """Send authenticate start.
652
+
653
+ Args:
654
+ method (str): Message method.
655
+ auth_data (Optional[str]): Authentication data.
656
+ initial_response (Optional[str]): Initial response.
657
+ """
658
+ msg = Message("Mysqlx.Session.AuthenticateStart")
659
+ msg["mech_name"] = method
660
+ if auth_data is not None:
661
+ msg["auth_data"] = auth_data
662
+ if initial_response is not None:
663
+ msg["initial_response"] = initial_response
664
+ self._writer.write_message(
665
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.SESS_AUTHENTICATE_START"),
666
+ msg,
667
+ )
668
+
669
+ def read_auth_continue(self) -> bytes:
670
+ """Read authenticate continue.
671
+
672
+ Raises:
673
+ :class:`InterfaceError`: If the message type is not
674
+ `Mysqlx.Session.AuthenticateContinue`
675
+
676
+ Returns:
677
+ str: The authentication data.
678
+ """
679
+ msg = self._reader.read_message()
680
+ while msg.type == "Mysqlx.Notice.Frame":
681
+ msg = self._reader.read_message()
682
+ if msg.type != "Mysqlx.Session.AuthenticateContinue":
683
+ raise InterfaceError(
684
+ "Unexpected message encountered during authentication handshake"
685
+ )
686
+ return msg["auth_data"]
687
+
688
+ def send_auth_continue(self, auth_data: str) -> None:
689
+ """Send authenticate continue.
690
+
691
+ Args:
692
+ auth_data (str): Authentication data.
693
+ """
694
+ msg = Message("Mysqlx.Session.AuthenticateContinue", auth_data=auth_data)
695
+ self._writer.write_message(
696
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.SESS_AUTHENTICATE_CONTINUE"),
697
+ msg,
698
+ )
699
+
700
+ def read_auth_ok(self) -> None:
701
+ """Read authenticate OK.
702
+
703
+ Raises:
704
+ :class:`mysqlx.InterfaceError`: If message type is `Mysqlx.Error`.
705
+ """
706
+ while True:
707
+ msg = self._reader.read_message()
708
+ if msg.type == "Mysqlx.Session.AuthenticateOk":
709
+ break
710
+ if msg.type == "Mysqlx.Error":
711
+ raise InterfaceError(msg.msg)
712
+
713
+ def send_prepare_prepare(
714
+ self,
715
+ msg_type: str,
716
+ msg: MessageType,
717
+ stmt: Union[
718
+ FindStatement,
719
+ DeleteStatement,
720
+ ModifyStatement,
721
+ ReadStatement,
722
+ RemoveStatement,
723
+ UpdateStatement,
724
+ ],
725
+ ) -> None:
726
+ """
727
+ Send prepare statement.
728
+
729
+ Args:
730
+ msg_type (str): Message ID string.
731
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
732
+ stmt (Statement): A `Statement` based type object.
733
+
734
+ Raises:
735
+ :class:`mysqlx.NotSupportedError`: If prepared statements are not
736
+ supported.
737
+
738
+ .. versionadded:: 8.0.16
739
+ """
740
+ if stmt.has_limit and msg.type != "Mysqlx.Crud.Insert":
741
+ # Remove 'limit' from message by building a new one
742
+ if msg.type == "Mysqlx.Crud.Find":
743
+ _, msg = self.build_find(stmt) # type: ignore[arg-type]
744
+ elif msg.type == "Mysqlx.Crud.Update":
745
+ _, msg = self.build_update(stmt) # type: ignore[arg-type]
746
+ elif msg.type == "Mysqlx.Crud.Delete":
747
+ _, msg = self.build_delete(stmt) # type: ignore[arg-type]
748
+ else:
749
+ raise ValueError(f"Invalid message type: {msg_type}")
750
+ # Build 'limit_expr' message
751
+ position = len(stmt.get_bindings())
752
+ placeholder = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.PLACEHOLDER")
753
+ msg_limit_expr = Message("Mysqlx.Crud.LimitExpr")
754
+ msg_limit_expr["row_count"] = Message(
755
+ "Mysqlx.Expr.Expr", type=placeholder, position=position
756
+ )
757
+ if msg.type == "Mysqlx.Crud.Find":
758
+ msg_limit_expr["offset"] = Message(
759
+ "Mysqlx.Expr.Expr", type=placeholder, position=position + 1
760
+ )
761
+ msg["limit_expr"] = msg_limit_expr
762
+
763
+ oneof_type, oneof_op = CRUD_PREPARE_MAPPING[msg_type]
764
+ msg_oneof = Message("Mysqlx.Prepare.Prepare.OneOfMessage")
765
+ msg_oneof["type"] = mysqlxpb_enum(oneof_type)
766
+ msg_oneof[oneof_op] = msg
767
+ msg_prepare = Message("Mysqlx.Prepare.Prepare")
768
+ msg_prepare["stmt_id"] = stmt.stmt_id
769
+ msg_prepare["stmt"] = msg_oneof
770
+
771
+ self._writer.write_message(
772
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.PREPARE_PREPARE"),
773
+ msg_prepare,
774
+ )
775
+
776
+ try:
777
+ self.read_ok()
778
+ except InterfaceError as err:
779
+ raise NotSupportedError from err
780
+
781
+ def send_prepare_execute(
782
+ self, msg_type: str, msg: MessageType, stmt: FilterableStatement
783
+ ) -> None:
784
+ """
785
+ Send execute statement.
786
+
787
+ Args:
788
+ msg_type (str): Message ID string.
789
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
790
+ stmt (Statement): A `Statement` based type object.
791
+
792
+ .. versionadded:: 8.0.16
793
+ """
794
+ oneof_type, oneof_op = CRUD_PREPARE_MAPPING[msg_type]
795
+ msg_oneof = Message("Mysqlx.Prepare.Prepare.OneOfMessage")
796
+ msg_oneof["type"] = mysqlxpb_enum(oneof_type)
797
+ msg_oneof[oneof_op] = msg
798
+ msg_execute = Message("Mysqlx.Prepare.Execute")
799
+ msg_execute["stmt_id"] = stmt.stmt_id
800
+
801
+ args = self._get_binding_args(stmt, is_scalar=False)
802
+ if args:
803
+ msg_execute["args"].extend(args)
804
+
805
+ if stmt.has_limit:
806
+ msg_execute["args"].extend(
807
+ [
808
+ self._create_any(stmt.get_limit_row_count()).get_message(),
809
+ self._create_any(stmt.get_limit_offset()).get_message(),
810
+ ]
811
+ )
812
+
813
+ self._writer.write_message(
814
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.PREPARE_EXECUTE"),
815
+ msg_execute,
816
+ )
817
+
818
+ def send_prepare_deallocate(self, stmt_id: int) -> None:
819
+ """
820
+ Send prepare deallocate statement.
821
+
822
+ Args:
823
+ stmt_id (int): Statement ID.
824
+
825
+ .. versionadded:: 8.0.16
826
+ """
827
+ msg_dealloc = Message("Mysqlx.Prepare.Deallocate")
828
+ msg_dealloc["stmt_id"] = stmt_id
829
+ self._writer.write_message(
830
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.PREPARE_DEALLOCATE"),
831
+ msg_dealloc,
832
+ )
833
+ self.read_ok()
834
+
835
+ def send_msg_without_ps(
836
+ self,
837
+ msg_type: str,
838
+ msg: MessageType,
839
+ stmt: Union[FilterableStatement, SqlStatement],
840
+ ) -> None:
841
+ """
842
+ Send a message without prepared statements support.
843
+
844
+ Args:
845
+ msg_type (str): Message ID string.
846
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
847
+ stmt (Statement): A `Statement` based type object.
848
+
849
+ .. versionadded:: 8.0.16
850
+ """
851
+ if stmt.has_limit:
852
+ msg_limit = Message("Mysqlx.Crud.Limit")
853
+ msg_limit["row_count"] = stmt.get_limit_row_count() # type: ignore[union-attr]
854
+ if msg.type == "Mysqlx.Crud.Find":
855
+ msg_limit["offset"] = stmt.get_limit_offset() # type: ignore[union-attr]
856
+ msg["limit"] = msg_limit
857
+ is_scalar = msg_type != "Mysqlx.ClientMessages.Type.SQL_STMT_EXECUTE"
858
+ args = self._get_binding_args(stmt, is_scalar=is_scalar)
859
+ if args:
860
+ msg["args"].extend(args)
861
+ self.send_msg(msg_type, msg)
862
+
863
+ def send_msg(self, msg_type: str, msg: MessageType) -> None:
864
+ """
865
+ Send a message.
866
+
867
+ Args:
868
+ msg_type (str): Message ID string.
869
+ msg (mysqlx.protobuf.Message): MySQL X Protobuf Message.
870
+
871
+ .. versionadded:: 8.0.16
872
+ """
873
+ self._writer.write_message(mysqlxpb_enum(msg_type), msg)
874
+
875
+ def build_find(
876
+ self, stmt: Union[FindStatement, ReadStatement]
877
+ ) -> Tuple[str, MessageType]:
878
+ """Build find/read message.
879
+
880
+ Args:
881
+ stmt (Statement): A :class:`mysqlx.ReadStatement` or
882
+ :class:`mysqlx.FindStatement` object.
883
+
884
+ Returns:
885
+ (tuple): Tuple containing:
886
+
887
+ * `str`: Message ID string.
888
+ * :class:`mysqlx.protobuf.Message`: MySQL X Protobuf Message.
889
+
890
+ .. versionadded:: 8.0.16
891
+ """
892
+ data_model = mysqlxpb_enum(
893
+ "Mysqlx.Crud.DataModel.DOCUMENT"
894
+ if stmt.is_doc_based()
895
+ else "Mysqlx.Crud.DataModel.TABLE"
896
+ )
897
+ collection = Message(
898
+ "Mysqlx.Crud.Collection",
899
+ name=stmt.target.name,
900
+ schema=stmt.schema.name,
901
+ )
902
+ msg = Message("Mysqlx.Crud.Find", data_model=data_model, collection=collection)
903
+ if stmt.has_projection:
904
+ msg["projection"] = stmt.get_projection_expr()
905
+ self._apply_filter(msg, stmt)
906
+
907
+ if stmt.is_lock_exclusive():
908
+ msg["locking"] = mysqlxpb_enum("Mysqlx.Crud.Find.RowLock.EXCLUSIVE_LOCK")
909
+ elif stmt.is_lock_shared():
910
+ msg["locking"] = mysqlxpb_enum("Mysqlx.Crud.Find.RowLock.SHARED_LOCK")
911
+
912
+ if stmt.lock_contention.value > 0:
913
+ msg["locking_options"] = stmt.lock_contention.value
914
+
915
+ return "Mysqlx.ClientMessages.Type.CRUD_FIND", msg
916
+
917
+ def build_update(
918
+ self, stmt: Union[ModifyStatement, UpdateStatement]
919
+ ) -> Tuple[str, MessageType]:
920
+ """Build update message.
921
+
922
+ Args:
923
+ stmt (Statement): A :class:`mysqlx.ModifyStatement` or
924
+ :class:`mysqlx.UpdateStatement` object.
925
+
926
+ Returns:
927
+ (tuple): Tuple containing:
928
+
929
+ * `str`: Message ID string.
930
+ * :class:`mysqlx.protobuf.Message`: MySQL X Protobuf Message.
931
+
932
+ .. versionadded:: 8.0.16
933
+ """
934
+ data_model = mysqlxpb_enum(
935
+ "Mysqlx.Crud.DataModel.DOCUMENT"
936
+ if stmt.is_doc_based()
937
+ else "Mysqlx.Crud.DataModel.TABLE"
938
+ )
939
+ collection = Message(
940
+ "Mysqlx.Crud.Collection",
941
+ name=stmt.target.name,
942
+ schema=stmt.schema.name,
943
+ )
944
+ msg = Message(
945
+ "Mysqlx.Crud.Update", data_model=data_model, collection=collection
946
+ )
947
+ self._apply_filter(msg, stmt)
948
+ for _, update_op in stmt.get_update_ops().items():
949
+ operation = Message("Mysqlx.Crud.UpdateOperation")
950
+ operation["operation"] = update_op.update_type
951
+ operation["source"] = update_op.source
952
+ if update_op.value is not None:
953
+ operation["value"] = build_expr(update_op.value)
954
+ msg["operation"].extend([operation.get_message()])
955
+
956
+ return "Mysqlx.ClientMessages.Type.CRUD_UPDATE", msg
957
+
958
+ def build_delete(
959
+ self, stmt: Union[DeleteStatement, RemoveStatement]
960
+ ) -> Tuple[str, MessageType]:
961
+ """Build delete message.
962
+
963
+ Args:
964
+ stmt (Statement): A :class:`mysqlx.DeleteStatement` or
965
+ :class:`mysqlx.RemoveStatement` object.
966
+
967
+ Returns:
968
+ (tuple): Tuple containing:
969
+
970
+ * `str`: Message ID string.
971
+ * :class:`mysqlx.protobuf.Message`: MySQL X Protobuf Message.
972
+
973
+ .. versionadded:: 8.0.16
974
+ """
975
+ data_model = mysqlxpb_enum(
976
+ "Mysqlx.Crud.DataModel.DOCUMENT"
977
+ if stmt.is_doc_based()
978
+ else "Mysqlx.Crud.DataModel.TABLE"
979
+ )
980
+ collection = Message(
981
+ "Mysqlx.Crud.Collection",
982
+ name=stmt.target.name,
983
+ schema=stmt.schema.name,
984
+ )
985
+ msg = Message(
986
+ "Mysqlx.Crud.Delete", data_model=data_model, collection=collection
987
+ )
988
+ self._apply_filter(msg, stmt)
989
+ return "Mysqlx.ClientMessages.Type.CRUD_DELETE", msg
990
+
991
+ def build_execute_statement(
992
+ self,
993
+ namespace: str,
994
+ stmt: Union[str, StatementType],
995
+ fields: Optional[Dict[str, Any]] = None,
996
+ ) -> Tuple[str, MessageType]:
997
+ """Build execute statement.
998
+
999
+ Args:
1000
+ namespace (str): The namespace.
1001
+ stmt (Statement): A `Statement` based type object.
1002
+ fields (Optional[dict]): The message fields.
1003
+
1004
+ Returns:
1005
+ (tuple): Tuple containing:
1006
+
1007
+ * `str`: Message ID string.
1008
+ * :class:`mysqlx.protobuf.Message`: MySQL X Protobuf Message.
1009
+
1010
+ .. versionadded:: 8.0.16
1011
+ """
1012
+ msg = Message(
1013
+ "Mysqlx.Sql.StmtExecute",
1014
+ namespace=namespace,
1015
+ stmt=stmt,
1016
+ compact_metadata=False,
1017
+ )
1018
+
1019
+ if fields:
1020
+ obj_flds = []
1021
+ for key, value in fields.items():
1022
+ obj_fld = Message(
1023
+ "Mysqlx.Datatypes.Object.ObjectField",
1024
+ key=key,
1025
+ value=self._create_any(value),
1026
+ )
1027
+ obj_flds.append(obj_fld.get_message())
1028
+ msg_obj = Message("Mysqlx.Datatypes.Object", fld=obj_flds)
1029
+ msg_any = Message("Mysqlx.Datatypes.Any", type=2, obj=msg_obj)
1030
+ msg["args"] = [msg_any.get_message()]
1031
+ return "Mysqlx.ClientMessages.Type.SQL_STMT_EXECUTE", msg
1032
+
1033
+ @staticmethod
1034
+ def build_insert(
1035
+ stmt: Union[AddStatement, InsertStatement]
1036
+ ) -> Tuple[str, MessageType]:
1037
+ """Build insert statement.
1038
+
1039
+ Args:
1040
+ stmt (Statement): A :class:`mysqlx.AddStatement` or
1041
+ :class:`mysqlx.InsertStatement` object.
1042
+
1043
+ Returns:
1044
+ (tuple): Tuple containing:
1045
+
1046
+ * `str`: Message ID string.
1047
+ * :class:`mysqlx.protobuf.Message`: MySQL X Protobuf Message.
1048
+
1049
+ .. versionadded:: 8.0.16
1050
+ """
1051
+ data_model = mysqlxpb_enum(
1052
+ "Mysqlx.Crud.DataModel.DOCUMENT"
1053
+ if stmt.is_doc_based()
1054
+ else "Mysqlx.Crud.DataModel.TABLE"
1055
+ )
1056
+ collection = Message(
1057
+ "Mysqlx.Crud.Collection",
1058
+ name=stmt.target.name,
1059
+ schema=stmt.schema.name,
1060
+ )
1061
+ msg = Message(
1062
+ "Mysqlx.Crud.Insert", data_model=data_model, collection=collection
1063
+ )
1064
+
1065
+ if hasattr(stmt, "_fields"):
1066
+ for field in stmt._fields:
1067
+ expr = ExprParser(
1068
+ field, not stmt.is_doc_based()
1069
+ ).parse_table_insert_field()
1070
+ msg["projection"].extend([expr.get_message()])
1071
+
1072
+ for value in stmt.get_values():
1073
+ row = Message("Mysqlx.Crud.Insert.TypedRow")
1074
+ if isinstance(value, list):
1075
+ for val in value:
1076
+ row["field"].extend([build_expr(val).get_message()])
1077
+ else:
1078
+ row["field"].extend([build_expr(value).get_message()])
1079
+ msg["row"].extend([row.get_message()])
1080
+
1081
+ if hasattr(stmt, "is_upsert"):
1082
+ msg["upsert"] = stmt.is_upsert()
1083
+
1084
+ return "Mysqlx.ClientMessages.Type.CRUD_INSERT", msg
1085
+
1086
+ def close_result(self, result: ResultBaseType) -> None:
1087
+ """Close the result.
1088
+
1089
+ Args:
1090
+ result (Result): A `Result` based type object.
1091
+
1092
+ Raises:
1093
+ :class:`mysqlx.OperationalError`: If message read is None.
1094
+ """
1095
+ msg = self._read_message(result)
1096
+ if msg is not None:
1097
+ raise OperationalError("Expected to close the result")
1098
+
1099
+ def read_row(self, result: ResultBaseType) -> Optional[MessageType]:
1100
+ """Read row.
1101
+
1102
+ Args:
1103
+ result (Result): A `Result` based type object.
1104
+ """
1105
+ msg = self._read_message(result)
1106
+ if msg is None:
1107
+ return None
1108
+ if msg.type == "Mysqlx.Resultset.Row":
1109
+ return msg
1110
+ self._reader.push_message(msg)
1111
+ return None
1112
+
1113
+ def get_column_metadata(self, result: ResultBaseType) -> List[ColumnType]:
1114
+ """Returns column metadata.
1115
+
1116
+ Args:
1117
+ result (Result): A `Result` based type object.
1118
+
1119
+ Raises:
1120
+ :class:`mysqlx.InterfaceError`: If unexpected message.
1121
+ """
1122
+ columns = []
1123
+ while True:
1124
+ msg = self._read_message(result)
1125
+ if msg is None:
1126
+ break
1127
+ if msg.type == "Mysqlx.Resultset.Row":
1128
+ self._reader.push_message(msg)
1129
+ break
1130
+ if msg.type != "Mysqlx.Resultset.ColumnMetaData":
1131
+ raise InterfaceError("Unexpected msg type")
1132
+ col = Column(
1133
+ msg["type"],
1134
+ msg["catalog"],
1135
+ msg["schema"],
1136
+ msg["table"],
1137
+ msg["original_table"],
1138
+ msg["name"],
1139
+ msg["original_name"],
1140
+ msg.get("length", 21),
1141
+ msg.get("collation", 0),
1142
+ msg.get("fractional_digits", 0),
1143
+ msg.get("flags", 16),
1144
+ msg.get("content_type"),
1145
+ )
1146
+ columns.append(col)
1147
+ return columns
1148
+
1149
+ def read_ok(self) -> None:
1150
+ """Read OK.
1151
+
1152
+ Raises:
1153
+ :class:`mysqlx.InterfaceError`: If unexpected message.
1154
+ """
1155
+ msg = self._reader.read_message()
1156
+ if msg.type == "Mysqlx.Error":
1157
+ raise InterfaceError(f"Mysqlx.Error: {msg['msg']}", errno=msg["code"])
1158
+ if msg.type != "Mysqlx.Ok":
1159
+ raise InterfaceError("Unexpected message encountered")
1160
+
1161
+ def send_connection_close(self) -> None:
1162
+ """Send connection close."""
1163
+ msg = Message("Mysqlx.Connection.Close")
1164
+ self._writer.write_message(
1165
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.CON_CLOSE"), msg
1166
+ )
1167
+
1168
+ def send_close(self) -> None:
1169
+ """Send close."""
1170
+ msg = Message("Mysqlx.Session.Close")
1171
+ self._writer.write_message(
1172
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.SESS_CLOSE"), msg
1173
+ )
1174
+
1175
+ def send_expect_open(self) -> None:
1176
+ """Send expectation."""
1177
+ cond_key = mysqlxpb_enum("Mysqlx.Expect.Open.Condition.Key.EXPECT_FIELD_EXIST")
1178
+ msg_oc = Message("Mysqlx.Expect.Open.Condition")
1179
+ msg_oc["condition_key"] = cond_key
1180
+ msg_oc["condition_value"] = "6.1"
1181
+
1182
+ msg_eo = Message("Mysqlx.Expect.Open")
1183
+ msg_eo["cond"] = [msg_oc.get_message()]
1184
+
1185
+ self._writer.write_message(
1186
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.EXPECT_OPEN"), msg_eo
1187
+ )
1188
+
1189
+ def send_reset(self, keep_open: Optional[bool] = None) -> bool:
1190
+ """Send reset session message.
1191
+
1192
+ Returns:
1193
+ boolean: ``True`` if the server will keep the session open,
1194
+ otherwise ``False``.
1195
+ """
1196
+ msg = Message("Mysqlx.Session.Reset")
1197
+ if keep_open is None:
1198
+ try:
1199
+ # Send expectation: keep connection open
1200
+ self.send_expect_open()
1201
+ self.read_ok()
1202
+ keep_open = True
1203
+ except InterfaceError:
1204
+ # Expectation is unkown by this version of the server
1205
+ keep_open = False
1206
+ if keep_open:
1207
+ msg["keep_open"] = True
1208
+ self._writer.write_message(
1209
+ mysqlxpb_enum("Mysqlx.ClientMessages.Type.SESS_RESET"), msg
1210
+ )
1211
+ self.read_ok()
1212
+ if keep_open:
1213
+ return True
1214
+ return False