Skip to content

Commit dcbe4d8

Browse files
isrgomeznmariz
authored andcommitted
WL14237: Support Query Attributes
This worklog defines how Connector Python Classic protocol will support and implement query attributes that apply to statements sent to the server for execution and available on MySQL v8.0.25 and up. The query attributes are additional fragments of data that consists of a value, a type of value and a name that can be used to access this value on the server's side.
1 parent 80eeb3a commit dcbe4d8

File tree

10 files changed

+789
-43
lines changed

10 files changed

+789
-43
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ Copyright (c) 2009, 2021, Oracle and/or its affiliates.
88
Full release notes:
99
http://dev.mysql.com/doc/relnotes/connector-python/en/
1010

11+
v8.0.26
12+
=======
13+
14+
- WL#14542: Deprecate TLS 1.0 and 1.1
15+
- WL#14440: Support for authentication_kerberos_client authentication plugin
16+
- WL#14237: Support query attributes
17+
1118
v8.0.25
1219
=======
1320

lib/mysql/connector/abstracts.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2014, 2020, Oracle and/or its affiliates.
1+
# Copyright (c) 2014, 2021, Oracle and/or its affiliates.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -29,9 +29,11 @@
2929
"""Module gathering all abstract base classes"""
3030

3131
from abc import ABCMeta, abstractmethod, abstractproperty
32+
from decimal import Decimal
33+
from time import sleep
34+
from datetime import date, datetime, time, timedelta
3235
import os
3336
import re
34-
import time
3537
import weakref
3638
TLS_V1_3_SUPPORTED = False
3739
try:
@@ -68,6 +70,9 @@
6870
'is optional and if it is not given will be assumed to belong to the '
6971
'default realm, as configured in the krb5.conf file.')
7072

73+
MYSQL_PY_TYPES = (
74+
(int, str, bytes, Decimal, float, datetime, date, timedelta, time,))
75+
7176

7277
@make_abc(ABCMeta)
7378
class MySQLConnectionAbstract(object):
@@ -111,6 +116,7 @@ def __init__(self, **kwargs):
111116
DEFAULT_CONFIGURATION["allow_local_infile_in_path"])
112117

113118
self._prepared_statements = None
119+
self._query_attrs = []
114120

115121
self._ssl_active = False
116122
self._auth_plugin = None
@@ -1033,7 +1039,7 @@ def reconnect(self, attempts=1, delay=0):
10331039
"attempt(s): {1}".format(attempts, str(err))
10341040
raise errors.InterfaceError(msg)
10351041
if delay > 0:
1036-
time.sleep(delay)
1042+
sleep(delay)
10371043

10381044
@abstractmethod
10391045
def is_connected(self):
@@ -1453,3 +1459,30 @@ def lastrowid(self):
14531459
def fetchwarnings(self):
14541460
"""Returns Warnings."""
14551461
return self._warnings
1462+
1463+
def get_attributes(self):
1464+
"""Get the added query attributes so far."""
1465+
if hasattr(self, "_cnx"):
1466+
return self._cnx._query_attrs
1467+
elif hasattr(self, "_connection"):
1468+
return self._connection._query_attrs
1469+
1470+
def add_attribute(self, name, value):
1471+
"""Add a query attribute and his value."""
1472+
if not isinstance(name, str):
1473+
raise errors.ProgrammingError(
1474+
"Parameter `name` must be a string type.")
1475+
if value is not None and not isinstance(value, MYSQL_PY_TYPES):
1476+
raise errors.ProgrammingError(
1477+
f"Object {value} cannot be converted to a MySQL type.")
1478+
if hasattr(self, "_cnx"):
1479+
self._cnx._query_attrs.append((name, value))
1480+
elif hasattr(self, "_connection"):
1481+
self._connection._query_attrs.append((name, value))
1482+
1483+
def clear_attributes(self):
1484+
"""Remove all the query attributes."""
1485+
if hasattr(self, "_cnx"):
1486+
self._cnx._query_attrs = []
1487+
elif hasattr(self, "_connection"):
1488+
self._connection._query_attrs = []

lib/mysql/connector/connection.py

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@
2929
"""Implementing communication with MySQL servers.
3030
"""
3131

32+
from decimal import Decimal
3233
from io import IOBase
34+
import datetime
3335
import logging
3436
import os
3537
import platform
3638
import socket
39+
import struct
3740
import time
3841
import warnings
3942

4043
from .authentication import get_auth_plugin
4144
from .constants import (
42-
ClientFlag, ServerCmd, ServerFlag,
45+
ClientFlag, ServerCmd, ServerFlag, FieldType,
4346
flag_is_set, ShutdownType, NET_BUFFER_LENGTH
4447
)
4548

@@ -52,7 +55,7 @@
5255
MySQLCursorBufferedNamedTuple)
5356
from .network import MySQLUnixSocket, MySQLTCPSocket
5457
from .protocol import MySQLProtocol
55-
from .utils import int4store, linux_distribution
58+
from .utils import int1store, int4store, lc_int, linux_distribution
5659
from .abstracts import MySQLConnectionAbstract
5760

5861
logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -102,6 +105,7 @@ def __init__(self, *args, **kwargs):
102105
self._auth_plugin = None
103106
self._krb_service_principal = None
104107
self._pool_config_version = None
108+
self._query_attrs_supported = False
105109

106110
self._columns_desc = []
107111

@@ -179,6 +183,10 @@ def _do_handshake(self):
179183
if handshake['capabilities'] & ClientFlag.PLUGIN_AUTH:
180184
self.set_client_flags([ClientFlag.PLUGIN_AUTH])
181185

186+
if handshake['capabilities'] & ClientFlag.CLIENT_QUERY_ATTRIBUTES:
187+
self._query_attrs_supported = True
188+
self.set_client_flags([ClientFlag.CLIENT_QUERY_ATTRIBUTES])
189+
182190
self._handshake = handshake
183191

184192
def _do_auth(self, username=None, password=None, database=None,
@@ -755,8 +763,85 @@ def cmd_query(self, query, raw=False, buffered=False, raw_as_string=False):
755763
756764
Returns a tuple()
757765
"""
758-
if not isinstance(query, bytes):
759-
query = query.encode('utf-8')
766+
if not isinstance(query, bytearray):
767+
if isinstance(query, str):
768+
query = query.encode('utf-8')
769+
query = bytearray(query)
770+
# Prepare query attrs
771+
charset = self.charset if self.charset != "utf8mb4" else "utf8"
772+
packet = bytearray()
773+
if not self._query_attrs_supported and self._query_attrs:
774+
warnings.warn(
775+
"This version of the server does not support Query Attributes",
776+
category=Warning)
777+
if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES:
778+
names = []
779+
types = []
780+
values = []
781+
null_bitmap = [0] * ((len(self._query_attrs) + 7) // 8)
782+
for pos, attr_tuple in enumerate(self._query_attrs):
783+
value = attr_tuple[1]
784+
flags = 0
785+
if value is None:
786+
null_bitmap[(pos // 8)] |= 1 << (pos % 8)
787+
types.append(int1store(FieldType.NULL) +
788+
int1store(flags))
789+
continue
790+
elif isinstance(value, int):
791+
(packed, field_type,
792+
flags) = self._protocol._prepare_binary_integer(value)
793+
values.append(packed)
794+
elif isinstance(value, str):
795+
value = value.encode(charset)
796+
values.append(lc_int(len(value)) + value)
797+
field_type = FieldType.VARCHAR
798+
elif isinstance(value, bytes):
799+
values.append(lc_int(len(value)) + value)
800+
field_type = FieldType.BLOB
801+
elif isinstance(value, Decimal):
802+
values.append(
803+
lc_int(len(str(value).encode(
804+
charset))) + str(value).encode(charset))
805+
field_type = FieldType.DECIMAL
806+
elif isinstance(value, float):
807+
values.append(struct.pack('<d', value))
808+
field_type = FieldType.DOUBLE
809+
elif isinstance(value, (datetime.datetime, datetime.date)):
810+
(packed, field_type) = \
811+
self._protocol._prepare_binary_timestamp(value)
812+
values.append(packed)
813+
elif isinstance(value, (datetime.timedelta, datetime.time)):
814+
(packed, field_type) = \
815+
self._protocol._prepare_binary_time(value)
816+
values.append(packed)
817+
else:
818+
raise errors.ProgrammingError(
819+
"MySQL binary protocol can not handle "
820+
"'{classname}' objects".format(
821+
classname=value.__class__.__name__))
822+
types.append(int1store(field_type) +
823+
int1store(flags))
824+
name = attr_tuple[0].encode(charset)
825+
names.append(lc_int(len(name)) + name)
826+
827+
# int<lenenc> parameter_count Number of parameters
828+
packet.extend(lc_int(len(self._query_attrs)))
829+
# int<lenenc> parameter_set_count Number of parameter sets.
830+
# Currently always 1
831+
packet.extend(lc_int(1))
832+
if values:
833+
packet.extend(
834+
b''.join([struct.pack('B', bit) for bit in null_bitmap]) +
835+
int1store(1))
836+
for _type, name in zip(types, names):
837+
packet.extend(_type)
838+
packet.extend(name)
839+
840+
for value in values:
841+
packet.extend(value)
842+
843+
packet.extend(query)
844+
query = bytes(packet)
760845
try:
761846
result = self._handle_result(self._send_cmd(ServerCmd.QUERY, query))
762847
except errors.ProgrammingError as err:
@@ -789,13 +874,23 @@ def cmd_query_iter(self, statements):
789874
790875
Returns a generator.
791876
"""
877+
packet = bytearray()
792878
if not isinstance(statements, bytearray):
793879
if isinstance(statements, str):
794880
statements = statements.encode('utf8')
795881
statements = bytearray(statements)
796882

883+
if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES:
884+
# int<lenenc> parameter_count Number of parameters
885+
packet.extend(lc_int(0))
886+
# int<lenenc> parameter_set_count Number of parameter sets.
887+
# Currently always 1
888+
packet.extend(lc_int(1))
889+
890+
packet.extend(statements)
891+
query = bytes(packet)
797892
# Handle the first query result
798-
yield self._handle_result(self._send_cmd(ServerCmd.QUERY, statements))
893+
yield self._handle_result(self._send_cmd(ServerCmd.QUERY, query))
799894

800895
# Handle next results, if any
801896
while self._have_next_result:
@@ -1266,10 +1361,18 @@ def cmd_stmt_execute(self, statement_id, data=(), parameters=(), flags=0):
12661361
self.cmd_stmt_send_long_data(statement_id, param_id,
12671362
data[param_id])
12681363
long_data_used[param_id] = (binary,)
1269-
1270-
execute_packet = self._protocol.make_stmt_execute(
1271-
statement_id, data, tuple(parameters), flags,
1272-
long_data_used, self.charset)
1364+
if not self._query_attrs_supported and self._query_attrs:
1365+
warnings.warn(
1366+
"This version of the server does not support Query Attributes",
1367+
category=Warning)
1368+
if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES:
1369+
execute_packet = self._protocol.make_stmt_execute(
1370+
statement_id, data, tuple(parameters), flags,
1371+
long_data_used, self.charset, self._query_attrs)
1372+
else:
1373+
execute_packet = self._protocol.make_stmt_execute(
1374+
statement_id, data, tuple(parameters), flags,
1375+
long_data_used, self.charset)
12731376
packet = self._send_cmd(ServerCmd.STMT_EXECUTE, packet=execute_packet)
12741377
result = self._handle_binary_result(packet)
12751378
return result

lib/mysql/connector/connection_cext.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def _open_connection(self):
205205
'ssl_disabled': True,
206206
"conn_attrs": self._conn_attrs,
207207
"local_infile": self._allow_local_infile,
208-
"load_data_local_dir": self._allow_local_infile_in_path
208+
"load_data_local_dir": self._allow_local_infile_in_path,
209209
}
210210

211211
tls_versions = self._ssl.get('tls_versions')
@@ -512,7 +512,8 @@ def cmd_query(self, query, raw=None, buffered=False, raw_as_string=False):
512512
query = query.encode('utf-8')
513513
self._cmysql.query(query,
514514
raw=raw, buffered=buffered,
515-
raw_as_string=raw_as_string)
515+
raw_as_string=raw_as_string,
516+
query_attrs=self._query_attrs)
516517
except MySQLInterfaceError as exc:
517518
raise errors.get_mysql_exception(exc.errno, msg=exc.msg,
518519
sqlstate=exc.sqlstate)

lib/mysql/connector/constants.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
MAX_PACKET_LENGTH = 16777215
4040
NET_BUFFER_LENGTH = 8192
4141
MAX_MYSQL_TABLE_COLUMNS = 4096
42+
# Flag used to send the Query Attributes with 0 (or more) parameters.
43+
PARAMETER_COUNT_AVAILABLE = 8
4244

4345
DEFAULT_CONFIGURATION = {
4446
'database': None,
@@ -424,6 +426,7 @@ class ClientFlag(_Flags):
424426
CAN_HANDLE_EXPIRED_PASSWORDS = 1 << 22
425427
SESION_TRACK = 1 << 23
426428
DEPRECATE_EOF = 1 << 24
429+
CLIENT_QUERY_ATTRIBUTES = 1 << 27
427430
SSL_VERIFY_SERVER_CERT = 1 << 30
428431
REMEMBER_OPTIONS = 1 << 31
429432

@@ -454,6 +457,7 @@ class ClientFlag(_Flags):
454457
'CAN_HANDLE_EXPIRED_PASSWORDS': (1 << 22, "Don't close the connection for a connection with expired password"),
455458
'SESION_TRACK': (1 << 23, 'Capable of handling server state change information'),
456459
'DEPRECATE_EOF': (1 << 24, 'Client no longer needs EOF packet'),
460+
'CLIENT_QUERY_ATTRIBUTES': (1 << 27, 'Support optional extension for query parameters'),
457461
'SSL_VERIFY_SERVER_CERT': (1 << 30, ''),
458462
'REMEMBER_OPTIONS': (1 << 31, ''),
459463
}
@@ -467,7 +471,7 @@ class ClientFlag(_Flags):
467471
SECURE_CONNECTION,
468472
MULTI_STATEMENTS,
469473
MULTI_RESULTS,
470-
CONNECT_ARGS
474+
CONNECT_ARGS,
471475
]
472476

473477
@classmethod

0 commit comments

Comments
 (0)