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/statement.py ADDED
@@ -0,0 +1,1542 @@
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
+ # mypy: disable-error-code="return-value"
30
+
31
+ """Implementation of Statements."""
32
+
33
+ from __future__ import annotations
34
+
35
+ import copy
36
+ import json
37
+ import warnings
38
+
39
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
40
+
41
+ from .constants import LockContention
42
+ from .dbdoc import DbDoc
43
+ from .errors import NotSupportedError, ProgrammingError
44
+ from .expr import ExprParser
45
+ from .helpers import deprecated
46
+ from .protobuf import mysqlxpb_enum
47
+ from .result import DocResult, Result, RowResult, SqlResult
48
+ from .types import (
49
+ ConnectionType,
50
+ DatabaseTargetType,
51
+ MessageType,
52
+ ProtobufMessageCextType,
53
+ ProtobufMessageType,
54
+ SchemaType,
55
+ )
56
+
57
+ ERR_INVALID_INDEX_NAME = 'The given index name "{}" is not valid'
58
+
59
+
60
+ class Expr:
61
+ """Expression wrapper."""
62
+
63
+ def __init__(self, expr: Any) -> None:
64
+ self.expr: Any = expr
65
+
66
+
67
+ def flexible_params(*values: Any) -> Union[List, Tuple]:
68
+ """Parse flexible parameters."""
69
+ if len(values) == 1 and isinstance(values[0], (list, tuple)):
70
+ return values[0]
71
+ return values
72
+
73
+
74
+ def is_quoted_identifier(identifier: str, sql_mode: str = "") -> bool:
75
+ """Check if the given identifier is quoted.
76
+
77
+ Args:
78
+ identifier (string): Identifier to check.
79
+ sql_mode (Optional[string]): SQL mode.
80
+
81
+ Returns:
82
+ `True` if the identifier has backtick quotes, and False otherwise.
83
+ """
84
+ if "ANSI_QUOTES" in sql_mode:
85
+ return (identifier[0] == "`" and identifier[-1] == "`") or (
86
+ identifier[0] == '"' and identifier[-1] == '"'
87
+ )
88
+ return identifier[0] == "`" and identifier[-1] == "`"
89
+
90
+
91
+ def quote_identifier(identifier: str, sql_mode: str = "") -> str:
92
+ """Quote the given identifier with backticks, converting backticks (`) in
93
+ the identifier name with the correct escape sequence (``).
94
+
95
+ Args:
96
+ identifier (string): Identifier to quote.
97
+ sql_mode (Optional[string]): SQL mode.
98
+
99
+ Returns:
100
+ A string with the identifier quoted with backticks.
101
+ """
102
+ if len(identifier) == 0:
103
+ return "``"
104
+ if "ANSI_QUOTES" in sql_mode:
105
+ quoted = identifier.replace('"', '""')
106
+ return f'"{quoted}"'
107
+ quoted = identifier.replace("`", "``")
108
+ return f"`{quoted}`"
109
+
110
+
111
+ def quote_multipart_identifier(identifiers: Iterable[str], sql_mode: str = "") -> str:
112
+ """Quote the given multi-part identifier with backticks.
113
+
114
+ Args:
115
+ identifiers (iterable): List of identifiers to quote.
116
+ sql_mode (Optional[string]): SQL mode.
117
+
118
+ Returns:
119
+ A string with the multi-part identifier quoted with backticks.
120
+ """
121
+ return ".".join(
122
+ [quote_identifier(identifier, sql_mode) for identifier in identifiers]
123
+ )
124
+
125
+
126
+ def parse_table_name(
127
+ default_schema: str, table_name: str, sql_mode: str = ""
128
+ ) -> Tuple[str, str]:
129
+ """Parse table name.
130
+
131
+ Args:
132
+ default_schema (str): The default schema.
133
+ table_name (str): The table name.
134
+ sql_mode(Optional[str]): The SQL mode.
135
+
136
+ Returns:
137
+ str: The parsed table name.
138
+ """
139
+ quote = '"' if "ANSI_QUOTES" in sql_mode else "`"
140
+ delimiter = f".{quote}" if quote in table_name else "."
141
+ temp = table_name.split(delimiter, 1)
142
+ return (
143
+ default_schema if len(temp) == 1 else temp[0].strip(quote),
144
+ temp[-1].strip(quote),
145
+ )
146
+
147
+
148
+ class Statement:
149
+ """Provides base functionality for statement objects.
150
+
151
+ Args:
152
+ target (object): The target database object, it can be
153
+ :class:`mysqlx.Collection` or :class:`mysqlx.Table`.
154
+ doc_based (bool): `True` if it is document based.
155
+ """
156
+
157
+ def __init__(self, target: DatabaseTargetType, doc_based: bool = True) -> None:
158
+ self._target: DatabaseTargetType = target
159
+ self._doc_based: bool = doc_based
160
+ self._connection: Optional[ConnectionType] = (
161
+ target.get_connection() if target else None
162
+ )
163
+ self._stmt_id: Optional[int] = None
164
+ self._exec_counter: int = 0
165
+ self._changed: bool = True
166
+ self._prepared: bool = False
167
+ self._deallocate_prepare_execute: bool = False
168
+
169
+ @property
170
+ def target(self) -> DatabaseTargetType:
171
+ """object: The database object target."""
172
+ return self._target
173
+
174
+ @property
175
+ def schema(self) -> SchemaType:
176
+ """:class:`mysqlx.Schema`: The Schema object."""
177
+ return self._target.schema
178
+
179
+ @property
180
+ def stmt_id(self) -> int:
181
+ """Returns this statement ID.
182
+
183
+ Returns:
184
+ int: The statement ID.
185
+ """
186
+ return self._stmt_id
187
+
188
+ @stmt_id.setter
189
+ def stmt_id(self, value: int) -> None:
190
+ self._stmt_id = value
191
+
192
+ @property
193
+ def exec_counter(self) -> int:
194
+ """int: The number of times this statement was executed."""
195
+ return self._exec_counter
196
+
197
+ @property
198
+ def changed(self) -> bool:
199
+ """bool: `True` if this statement has changes."""
200
+ return self._changed
201
+
202
+ @changed.setter
203
+ def changed(self, value: bool) -> None:
204
+ self._changed = value
205
+
206
+ @property
207
+ def prepared(self) -> bool:
208
+ """bool: `True` if this statement has been prepared."""
209
+ return self._prepared
210
+
211
+ @prepared.setter
212
+ def prepared(self, value: bool) -> None:
213
+ self._prepared = value
214
+
215
+ @property
216
+ def repeated(self) -> bool:
217
+ """bool: `True` if this statement was executed more than once."""
218
+ return self._exec_counter > 1
219
+
220
+ @property
221
+ def deallocate_prepare_execute(self) -> bool:
222
+ """bool: `True` to deallocate + prepare + execute statement."""
223
+ return self._deallocate_prepare_execute
224
+
225
+ @deallocate_prepare_execute.setter
226
+ def deallocate_prepare_execute(self, value: bool) -> None:
227
+ self._deallocate_prepare_execute = value
228
+
229
+ def is_doc_based(self) -> bool:
230
+ """Check if it is document based.
231
+
232
+ Returns:
233
+ bool: `True` if it is document based.
234
+ """
235
+ return self._doc_based
236
+
237
+ def increment_exec_counter(self) -> None:
238
+ """Increments the number of times this statement has been executed."""
239
+ self._exec_counter += 1
240
+
241
+ def reset_exec_counter(self) -> None:
242
+ """Resets the number of times this statement has been executed."""
243
+ self._exec_counter = 0
244
+
245
+ def execute(self) -> Any:
246
+ """Execute the statement.
247
+
248
+ Raises:
249
+ NotImplementedError: This method must be implemented.
250
+ """
251
+ raise NotImplementedError
252
+
253
+
254
+ class FilterableStatement(Statement):
255
+ """A statement to be used with filterable statements.
256
+
257
+ Args:
258
+ target (object): The target database object, it can be
259
+ :class:`mysqlx.Collection` or :class:`mysqlx.Table`.
260
+ doc_based (Optional[bool]): `True` if it is document based
261
+ (default: `True`).
262
+ condition (Optional[str]): Sets the search condition to filter
263
+ documents or records.
264
+ """
265
+
266
+ def __init__(
267
+ self,
268
+ target: DatabaseTargetType,
269
+ doc_based: bool = True,
270
+ condition: Optional[str] = None,
271
+ ) -> None:
272
+ super().__init__(target=target, doc_based=doc_based)
273
+ self._binding_map: Dict[str, Any] = {}
274
+ self._bindings: Union[Dict[str, Any], List] = {}
275
+ self._having: Optional[MessageType] = None
276
+ self._grouping_str: str = ""
277
+ self._grouping: Optional[
278
+ List[Union[ProtobufMessageType, ProtobufMessageCextType]]
279
+ ] = None
280
+ self._limit_offset: int = 0
281
+ self._limit_row_count: int = None
282
+ self._projection_str: str = ""
283
+ self._projection_expr: Optional[
284
+ List[Union[ProtobufMessageType, ProtobufMessageCextType]]
285
+ ] = None
286
+ self._sort_str: str = ""
287
+ self._sort_expr: Optional[
288
+ List[Union[ProtobufMessageType, ProtobufMessageCextType]]
289
+ ] = None
290
+ self._where_str: str = ""
291
+ self._where_expr: MessageType = None
292
+ self.has_bindings: bool = False
293
+ self.has_limit: bool = False
294
+ self.has_group_by: bool = False
295
+ self.has_having: bool = False
296
+ self.has_projection: bool = False
297
+ self.has_sort: bool = False
298
+ self.has_where: bool = False
299
+ if condition:
300
+ self._set_where(condition)
301
+
302
+ def _bind_single(self, obj: Union[DbDoc, Dict[str, Any], str]) -> None:
303
+ """Bind single object.
304
+
305
+ Args:
306
+ obj (:class:`mysqlx.DbDoc` or str): DbDoc or JSON string object.
307
+
308
+ Raises:
309
+ :class:`mysqlx.ProgrammingError`: If invalid JSON string to bind.
310
+ ValueError: If JSON loaded is not a dictionary.
311
+ """
312
+ if isinstance(obj, dict):
313
+ self.bind(DbDoc(obj).as_str())
314
+ elif isinstance(obj, DbDoc):
315
+ self.bind(obj.as_str())
316
+ elif isinstance(obj, str):
317
+ try:
318
+ res = json.loads(obj)
319
+ if not isinstance(res, dict):
320
+ raise ValueError
321
+ except ValueError as err:
322
+ raise ProgrammingError("Invalid JSON string to bind") from err
323
+ for key in res.keys():
324
+ self.bind(key, res[key])
325
+ else:
326
+ raise ProgrammingError("Invalid JSON string or object to bind")
327
+
328
+ def _sort(self, *clauses: str) -> FilterableStatement:
329
+ """Sets the sorting criteria.
330
+
331
+ Args:
332
+ *clauses: The expression strings defining the sort criteria.
333
+
334
+ Returns:
335
+ mysqlx.FilterableStatement: FilterableStatement object.
336
+ """
337
+ self.has_sort = True
338
+ self._sort_str = ",".join(flexible_params(*clauses))
339
+ self._sort_expr = ExprParser(
340
+ self._sort_str, not self._doc_based
341
+ ).parse_order_spec()
342
+ self._changed = True
343
+ return self
344
+
345
+ def _set_where(self, condition: str) -> FilterableStatement:
346
+ """Sets the search condition to filter.
347
+
348
+ Args:
349
+ condition (str): Sets the search condition to filter documents or
350
+ records.
351
+
352
+ Returns:
353
+ mysqlx.FilterableStatement: FilterableStatement object.
354
+ """
355
+ self.has_where = True
356
+ self._where_str = condition
357
+ try:
358
+ expr = ExprParser(condition, not self._doc_based)
359
+ self._where_expr = expr.expr()
360
+ except ValueError as err:
361
+ raise ProgrammingError("Invalid condition") from err
362
+ self._binding_map = expr.placeholder_name_to_position
363
+ self._changed = True
364
+ return self
365
+
366
+ def _set_group_by(self, *fields: str) -> None:
367
+ """Set group by.
368
+
369
+ Args:
370
+ *fields: List of fields.
371
+ """
372
+ fields = flexible_params(*fields)
373
+ self.has_group_by = True
374
+ self._grouping_str = ",".join(fields)
375
+ self._grouping = ExprParser(
376
+ self._grouping_str, not self._doc_based
377
+ ).parse_expr_list()
378
+ self._changed = True
379
+
380
+ def _set_having(self, condition: str) -> None:
381
+ """Set having.
382
+
383
+ Args:
384
+ condition (str): The condition.
385
+ """
386
+ self.has_having = True
387
+ self._having = ExprParser(condition, not self._doc_based).expr()
388
+ self._changed = True
389
+
390
+ def _set_projection(self, *fields: str) -> FilterableStatement:
391
+ """Set the projection.
392
+
393
+ Args:
394
+ *fields: List of fields.
395
+
396
+ Returns:
397
+ :class:`mysqlx.FilterableStatement`: Returns self.
398
+ """
399
+ fields = flexible_params(*fields)
400
+ self.has_projection = True
401
+ self._projection_str = ",".join(fields)
402
+ self._projection_expr = ExprParser(
403
+ self._projection_str, not self._doc_based
404
+ ).parse_table_select_projection()
405
+ self._changed = True
406
+ return self
407
+
408
+ def get_binding_map(self) -> Dict[str, Any]:
409
+ """Returns the binding map dictionary.
410
+
411
+ Returns:
412
+ dict: The binding map dictionary.
413
+ """
414
+ return self._binding_map
415
+
416
+ def get_bindings(self) -> Union[Dict[str, Any], List]:
417
+ """Returns the bindings list.
418
+
419
+ Returns:
420
+ `list`: The bindings list.
421
+ """
422
+ return self._bindings
423
+
424
+ def get_grouping(self) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
425
+ """Returns the grouping expression list.
426
+
427
+ Returns:
428
+ `list`: The grouping expression list.
429
+ """
430
+ return self._grouping
431
+
432
+ def get_having(self) -> MessageType:
433
+ """Returns the having expression.
434
+
435
+ Returns:
436
+ object: The having expression.
437
+ """
438
+ return self._having
439
+
440
+ def get_limit_row_count(self) -> int:
441
+ """Returns the limit row count.
442
+
443
+ Returns:
444
+ int: The limit row count.
445
+ """
446
+ return self._limit_row_count
447
+
448
+ def get_limit_offset(self) -> int:
449
+ """Returns the limit offset.
450
+
451
+ Returns:
452
+ int: The limit offset.
453
+ """
454
+ return self._limit_offset
455
+
456
+ def get_where_expr(self) -> MessageType:
457
+ """Returns the where expression.
458
+
459
+ Returns:
460
+ object: The where expression.
461
+ """
462
+ return self._where_expr
463
+
464
+ def get_projection_expr(
465
+ self,
466
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
467
+ """Returns the projection expression.
468
+
469
+ Returns:
470
+ object: The projection expression.
471
+ """
472
+ return self._projection_expr
473
+
474
+ def get_sort_expr(
475
+ self,
476
+ ) -> List[Union[ProtobufMessageType, ProtobufMessageCextType]]:
477
+ """Returns the sort expression.
478
+
479
+ Returns:
480
+ object: The sort expression.
481
+ """
482
+ return self._sort_expr
483
+
484
+ @deprecated("8.0.12")
485
+ def where(self, condition: str) -> FilterableStatement:
486
+ """Sets the search condition to filter.
487
+
488
+ Args:
489
+ condition (str): Sets the search condition to filter documents or
490
+ records.
491
+
492
+ Returns:
493
+ mysqlx.FilterableStatement: FilterableStatement object.
494
+
495
+ .. deprecated:: 8.0.12
496
+ """
497
+ return self._set_where(condition)
498
+
499
+ @deprecated("8.0.12")
500
+ def sort(self, *clauses: str) -> FilterableStatement:
501
+ """Sets the sorting criteria.
502
+
503
+ Args:
504
+ *clauses: The expression strings defining the sort criteria.
505
+
506
+ Returns:
507
+ mysqlx.FilterableStatement: FilterableStatement object.
508
+
509
+ .. deprecated:: 8.0.12
510
+ """
511
+ return self._sort(*clauses)
512
+
513
+ def limit(
514
+ self, row_count: int, offset: Optional[int] = None
515
+ ) -> FilterableStatement:
516
+ """Sets the maximum number of items to be returned.
517
+
518
+ Args:
519
+ row_count (int): The maximum number of items.
520
+
521
+ Returns:
522
+ mysqlx.FilterableStatement: FilterableStatement object.
523
+
524
+ Raises:
525
+ ValueError: If ``row_count`` is not a positive integer.
526
+
527
+ .. versionchanged:: 8.0.12
528
+ The usage of ``offset`` was deprecated.
529
+ """
530
+ if not isinstance(row_count, int) or row_count < 0:
531
+ raise ValueError("The 'row_count' value must be a positive integer")
532
+ if not self.has_limit:
533
+ self._changed = bool(self._exec_counter == 0)
534
+ self._deallocate_prepare_execute = bool(not self._exec_counter == 0)
535
+
536
+ self._limit_row_count = row_count
537
+ self.has_limit = True
538
+ if offset:
539
+ self.offset(offset)
540
+ warnings.warn(
541
+ "'limit(row_count, offset)' is deprecated, please "
542
+ "use 'offset(offset)' to set the number of items to "
543
+ "skip",
544
+ category=DeprecationWarning,
545
+ )
546
+ return self
547
+
548
+ def offset(self, offset: int) -> FilterableStatement:
549
+ """Sets the number of items to skip.
550
+
551
+ Args:
552
+ offset (int): The number of items to skip.
553
+
554
+ Returns:
555
+ mysqlx.FilterableStatement: FilterableStatement object.
556
+
557
+ Raises:
558
+ ValueError: If ``offset`` is not a positive integer.
559
+
560
+ .. versionadded:: 8.0.12
561
+ """
562
+ if not isinstance(offset, int) or offset < 0:
563
+ raise ValueError("The 'offset' value must be a positive integer")
564
+ self._limit_offset = offset
565
+ return self
566
+
567
+ def bind(self, *args: Any) -> FilterableStatement:
568
+ """Binds value(s) to a specific placeholder(s).
569
+
570
+ Args:
571
+ *args: The name of the placeholder and the value to bind.
572
+ A :class:`mysqlx.DbDoc` object or a JSON string
573
+ representation can be used.
574
+
575
+ Returns:
576
+ mysqlx.FilterableStatement: FilterableStatement object.
577
+
578
+ Raises:
579
+ ProgrammingError: If the number of arguments is invalid.
580
+ """
581
+ self.has_bindings = True
582
+ count = len(args)
583
+ if count == 1:
584
+ self._bind_single(args[0])
585
+ elif count == 2:
586
+ self._bindings[args[0]] = args[1]
587
+ else:
588
+ raise ProgrammingError("Invalid number of arguments to bind")
589
+ return self
590
+
591
+ def execute(self) -> Any:
592
+ """Execute the statement.
593
+
594
+ Raises:
595
+ NotImplementedError: This method must be implemented.
596
+ """
597
+ raise NotImplementedError
598
+
599
+
600
+ class SqlStatement(Statement):
601
+ """A statement for SQL execution.
602
+
603
+ Args:
604
+ connection (mysqlx.connection.Connection): Connection object.
605
+ sql (string): The sql statement to be executed.
606
+ """
607
+
608
+ def __init__(self, connection: ConnectionType, sql: str) -> None:
609
+ super().__init__(target=None, doc_based=False)
610
+ self._connection: ConnectionType = connection
611
+ self._sql: str = sql
612
+ self._binding_map: Optional[Dict[str, Any]] = None
613
+ self._bindings: Union[List, Tuple] = []
614
+ self.has_bindings: bool = False
615
+ self.has_limit: bool = False
616
+
617
+ @property
618
+ def sql(self) -> str:
619
+ """string: The SQL text statement."""
620
+ return self._sql
621
+
622
+ def get_binding_map(self) -> Dict[str, Any]:
623
+ """Returns the binding map dictionary.
624
+
625
+ Returns:
626
+ dict: The binding map dictionary.
627
+ """
628
+ return self._binding_map
629
+
630
+ def get_bindings(self) -> Union[Tuple, List]:
631
+ """Returns the bindings list.
632
+
633
+ Returns:
634
+ `list`: The bindings list.
635
+ """
636
+ return self._bindings
637
+
638
+ def bind(self, *args: Any) -> SqlStatement:
639
+ """Binds value(s) to a specific placeholder(s).
640
+
641
+ Args:
642
+ *args: The value(s) to bind.
643
+
644
+ Returns:
645
+ mysqlx.SqlStatement: SqlStatement object.
646
+ """
647
+ if len(args) == 0:
648
+ raise ProgrammingError("Invalid number of arguments to bind")
649
+ self.has_bindings = True
650
+ bindings = flexible_params(*args)
651
+ if isinstance(bindings, (list, tuple)):
652
+ self._bindings = bindings
653
+ else:
654
+ self._bindings.append(bindings)
655
+ return self
656
+
657
+ def execute(self) -> SqlResult:
658
+ """Execute the statement.
659
+
660
+ Returns:
661
+ mysqlx.SqlResult: SqlResult object.
662
+ """
663
+ return self._connection.send_sql(self)
664
+
665
+
666
+ class WriteStatement(Statement):
667
+ """Provide common write operation attributes."""
668
+
669
+ def __init__(self, target: DatabaseTargetType, doc_based: bool) -> None:
670
+ super().__init__(target, doc_based)
671
+ self._values: List[
672
+ Union[
673
+ int,
674
+ str,
675
+ DbDoc,
676
+ Dict[str, Any],
677
+ List[Optional[Union[str, int, float, ExprParser, Dict[str, Any]]]],
678
+ ]
679
+ ] = []
680
+
681
+ def get_values(
682
+ self,
683
+ ) -> List[
684
+ Union[
685
+ int,
686
+ str,
687
+ DbDoc,
688
+ Dict[str, Any],
689
+ List[Optional[Union[str, int, float, ExprParser, Dict[str, Any]]]],
690
+ ]
691
+ ]:
692
+ """Returns the list of values.
693
+
694
+ Returns:
695
+ `list`: The list of values.
696
+ """
697
+ return self._values
698
+
699
+ def execute(self) -> Any:
700
+ """Execute the statement.
701
+
702
+ Raises:
703
+ NotImplementedError: This method must be implemented.
704
+ """
705
+ raise NotImplementedError
706
+
707
+
708
+ class AddStatement(WriteStatement):
709
+ """A statement for document addition on a collection.
710
+
711
+ Args:
712
+ collection (mysqlx.Collection): The Collection object.
713
+ """
714
+
715
+ def __init__(self, collection: DatabaseTargetType) -> None:
716
+ super().__init__(collection, True)
717
+ self._upsert: bool = False
718
+ self.ids: List = []
719
+
720
+ def is_upsert(self) -> bool:
721
+ """Returns `True` if it's an upsert.
722
+
723
+ Returns:
724
+ bool: `True` if it's an upsert.
725
+ """
726
+ return self._upsert
727
+
728
+ def upsert(self, value: bool = True) -> AddStatement:
729
+ """Sets the upset flag to the boolean of the value provided.
730
+ Setting of this flag allows updating of the matched rows/documents
731
+ with the provided value.
732
+
733
+ Args:
734
+ value (optional[bool]): Set or unset the upsert flag.
735
+ """
736
+ self._upsert = value
737
+ return self
738
+
739
+ def add(self, *values: DbDoc) -> AddStatement:
740
+ """Adds a list of documents into a collection.
741
+
742
+ Args:
743
+ *values: The documents to be added into the collection.
744
+
745
+ Returns:
746
+ mysqlx.AddStatement: AddStatement object.
747
+ """
748
+ for val in flexible_params(*values):
749
+ if isinstance(val, DbDoc):
750
+ self._values.append(val)
751
+ else:
752
+ self._values.append(DbDoc(val))
753
+ return self
754
+
755
+ def execute(self) -> Result:
756
+ """Execute the statement.
757
+
758
+ Returns:
759
+ mysqlx.Result: Result object.
760
+ """
761
+ if len(self._values) == 0:
762
+ return Result()
763
+
764
+ return self._connection.send_insert(self)
765
+
766
+
767
+ class UpdateSpec:
768
+ """Update specification class implementation.
769
+
770
+ Args:
771
+ update_type (int): The update type.
772
+ source (str): The source.
773
+ value (Optional[str]): The value.
774
+
775
+ Raises:
776
+ ProgrammingError: If `source` is invalid.
777
+ """
778
+
779
+ def __init__(self, update_type: int, source: str, value: Any = None) -> None:
780
+ if update_type == mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.SET"):
781
+ self._table_set(source, value)
782
+ else:
783
+ self.update_type: int = update_type
784
+ try:
785
+ self.source: Any = ExprParser(source, False).document_field().identifier
786
+ except ValueError as err:
787
+ raise ProgrammingError(f"{err}") from err
788
+ self.value: Any = value
789
+
790
+ def _table_set(self, source: str, value: Any) -> None:
791
+ """Table set.
792
+
793
+ Args:
794
+ source (str): The source.
795
+ value (str): The value.
796
+ """
797
+ self.update_type = mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.SET")
798
+ self.source = ExprParser(source, True).parse_table_update_field()
799
+ self.value = value
800
+
801
+
802
+ class ModifyStatement(FilterableStatement):
803
+ """A statement for document update operations on a Collection.
804
+
805
+ Args:
806
+ collection (mysqlx.Collection): The Collection object.
807
+ condition (str): Sets the search condition to identify the documents
808
+ to be modified.
809
+
810
+ .. versionchanged:: 8.0.12
811
+ The ``condition`` parameter is now mandatory.
812
+ """
813
+
814
+ def __init__(self, collection: DatabaseTargetType, condition: str) -> None:
815
+ super().__init__(target=collection, condition=condition)
816
+ self._update_ops: Dict[str, Any] = {}
817
+
818
+ def sort(self, *clauses: str) -> ModifyStatement:
819
+ """Sets the sorting criteria.
820
+
821
+ Args:
822
+ *clauses: The expression strings defining the sort criteria.
823
+
824
+ Returns:
825
+ mysqlx.ModifyStatement: ModifyStatement object.
826
+ """
827
+ return self._sort(*clauses)
828
+
829
+ def get_update_ops(self) -> Dict[str, Any]:
830
+ """Returns the list of update operations.
831
+
832
+ Returns:
833
+ `list`: The list of update operations.
834
+ """
835
+ return self._update_ops
836
+
837
+ def set(self, doc_path: str, value: Any) -> ModifyStatement:
838
+ """Sets or updates attributes on documents in a collection.
839
+
840
+ Args:
841
+ doc_path (string): The document path of the item to be set.
842
+ value (string): The value to be set on the specified attribute.
843
+
844
+ Returns:
845
+ mysqlx.ModifyStatement: ModifyStatement object.
846
+ """
847
+ self._update_ops[doc_path] = UpdateSpec(
848
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.ITEM_SET"),
849
+ doc_path,
850
+ value,
851
+ )
852
+ self._changed = True
853
+ return self
854
+
855
+ @deprecated("8.0.12")
856
+ def change(self, doc_path: str, value: Any) -> ModifyStatement:
857
+ """Add an update to the statement setting the field, if it exists at
858
+ the document path, to the given value.
859
+
860
+ Args:
861
+ doc_path (string): The document path of the item to be set.
862
+ value (object): The value to be set on the specified attribute.
863
+
864
+ Returns:
865
+ mysqlx.ModifyStatement: ModifyStatement object.
866
+
867
+ .. deprecated:: 8.0.12
868
+ """
869
+ self._update_ops[doc_path] = UpdateSpec(
870
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.ITEM_REPLACE"),
871
+ doc_path,
872
+ value,
873
+ )
874
+ self._changed = True
875
+ return self
876
+
877
+ def unset(self, *doc_paths: str) -> ModifyStatement:
878
+ """Removes attributes from documents in a collection.
879
+
880
+ Args:
881
+ doc_paths (list): The list of document paths of the attributes to be
882
+ removed.
883
+
884
+ Returns:
885
+ mysqlx.ModifyStatement: ModifyStatement object.
886
+ """
887
+ for item in flexible_params(*doc_paths):
888
+ self._update_ops[item] = UpdateSpec(
889
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.ITEM_REMOVE"),
890
+ item,
891
+ )
892
+ self._changed = True
893
+ return self
894
+
895
+ def array_insert(self, field: str, value: Any) -> ModifyStatement:
896
+ """Insert a value into the specified array in documents of a
897
+ collection.
898
+
899
+ Args:
900
+ field (string): A document path that identifies the array attribute
901
+ and position where the value will be inserted.
902
+ value (object): The value to be inserted.
903
+
904
+ Returns:
905
+ mysqlx.ModifyStatement: ModifyStatement object.
906
+ """
907
+ self._update_ops[field] = UpdateSpec(
908
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.ARRAY_INSERT"),
909
+ field,
910
+ value,
911
+ )
912
+ self._changed = True
913
+ return self
914
+
915
+ def array_append(self, doc_path: str, value: Any) -> ModifyStatement:
916
+ """Inserts a value into a specific position in an array attribute in
917
+ documents of a collection.
918
+
919
+ Args:
920
+ doc_path (string): A document path that identifies the array
921
+ attribute and position where the value will be
922
+ inserted.
923
+ value (object): The value to be inserted.
924
+
925
+ Returns:
926
+ mysqlx.ModifyStatement: ModifyStatement object.
927
+ """
928
+ self._update_ops[doc_path] = UpdateSpec(
929
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.ARRAY_APPEND"),
930
+ doc_path,
931
+ value,
932
+ )
933
+ self._changed = True
934
+ return self
935
+
936
+ def patch(self, doc: Union[Dict, DbDoc, ExprParser, str]) -> ModifyStatement:
937
+ """Takes a :class:`mysqlx.DbDoc`, string JSON format or a dict with the
938
+ changes and applies it on all matching documents.
939
+
940
+ Args:
941
+ doc (object): A generic document (DbDoc), string in JSON format or
942
+ dict, with the changes to apply to the matching
943
+ documents.
944
+
945
+ Returns:
946
+ mysqlx.ModifyStatement: ModifyStatement object.
947
+ """
948
+ if doc is None:
949
+ doc = ""
950
+ if not isinstance(doc, (ExprParser, dict, DbDoc, str)):
951
+ raise ProgrammingError(
952
+ "Invalid data for update operation on document collection table"
953
+ )
954
+ self._update_ops["patch"] = UpdateSpec(
955
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.MERGE_PATCH"),
956
+ "$",
957
+ doc.expr() if isinstance(doc, ExprParser) else doc,
958
+ )
959
+ self._changed = True
960
+ return self
961
+
962
+ def execute(self) -> Result:
963
+ """Execute the statement.
964
+
965
+ Returns:
966
+ mysqlx.Result: Result object.
967
+
968
+ Raises:
969
+ ProgrammingError: If condition was not set.
970
+ """
971
+ if not self.has_where:
972
+ raise ProgrammingError("No condition was found for modify")
973
+ return self._connection.send_update(self)
974
+
975
+
976
+ class ReadStatement(FilterableStatement):
977
+ """Provide base functionality for Read operations
978
+
979
+ Args:
980
+ target (object): The target database object, it can be
981
+ :class:`mysqlx.Collection` or :class:`mysqlx.Table`.
982
+ doc_based (Optional[bool]): `True` if it is document based
983
+ (default: `True`).
984
+ condition (Optional[str]): Sets the search condition to filter
985
+ documents or records.
986
+ """
987
+
988
+ def __init__(
989
+ self,
990
+ target: DatabaseTargetType,
991
+ doc_based: bool = True,
992
+ condition: Optional[str] = None,
993
+ ) -> None:
994
+ super().__init__(target, doc_based, condition)
995
+ self._lock_exclusive: bool = False
996
+ self._lock_shared: bool = False
997
+ self._lock_contention: LockContention = LockContention.DEFAULT
998
+
999
+ @property
1000
+ def lock_contention(self) -> LockContention:
1001
+ """:class:`mysqlx.LockContention`: The lock contention value."""
1002
+ return self._lock_contention
1003
+
1004
+ def _set_lock_contention(self, lock_contention: LockContention) -> None:
1005
+ """Set the lock contention.
1006
+
1007
+ Args:
1008
+ lock_contention (:class:`mysqlx.LockContention`): Lock contention.
1009
+
1010
+ Raises:
1011
+ ProgrammingError: If is an invalid lock contention value.
1012
+ """
1013
+ try:
1014
+ # Check if is a valid lock contention value
1015
+ _ = LockContention(lock_contention.value)
1016
+ except ValueError as err:
1017
+ raise ProgrammingError(
1018
+ "Invalid lock contention mode. Use 'NOWAIT' or 'SKIP_LOCKED'"
1019
+ ) from err
1020
+ self._lock_contention = lock_contention
1021
+
1022
+ def is_lock_exclusive(self) -> bool:
1023
+ """Returns `True` if is `EXCLUSIVE LOCK`.
1024
+
1025
+ Returns:
1026
+ bool: `True` if is `EXCLUSIVE LOCK`.
1027
+ """
1028
+ return self._lock_exclusive
1029
+
1030
+ def is_lock_shared(self) -> bool:
1031
+ """Returns `True` if is `SHARED LOCK`.
1032
+
1033
+ Returns:
1034
+ bool: `True` if is `SHARED LOCK`.
1035
+ """
1036
+ return self._lock_shared
1037
+
1038
+ def lock_shared(
1039
+ self, lock_contention: LockContention = LockContention.DEFAULT
1040
+ ) -> ReadStatement:
1041
+ """Execute a read operation with `SHARED LOCK`. Only one lock can be
1042
+ active at a time.
1043
+
1044
+ Args:
1045
+ lock_contention (:class:`mysqlx.LockContention`): Lock contention.
1046
+ """
1047
+ self._lock_exclusive = False
1048
+ self._lock_shared = True
1049
+ self._set_lock_contention(lock_contention)
1050
+ return self
1051
+
1052
+ def lock_exclusive(
1053
+ self, lock_contention: LockContention = LockContention.DEFAULT
1054
+ ) -> ReadStatement:
1055
+ """Execute a read operation with `EXCLUSIVE LOCK`. Only one lock can be
1056
+ active at a time.
1057
+
1058
+ Args:
1059
+ lock_contention (:class:`mysqlx.LockContention`): Lock contention.
1060
+ """
1061
+ self._lock_exclusive = True
1062
+ self._lock_shared = False
1063
+ self._set_lock_contention(lock_contention)
1064
+ return self
1065
+
1066
+ def group_by(self, *fields: str) -> ReadStatement:
1067
+ """Sets a grouping criteria for the resultset.
1068
+
1069
+ Args:
1070
+ *fields: The string expressions identifying the grouping criteria.
1071
+
1072
+ Returns:
1073
+ mysqlx.ReadStatement: ReadStatement object.
1074
+ """
1075
+ self._set_group_by(*fields)
1076
+ return self
1077
+
1078
+ def having(self, condition: str) -> ReadStatement:
1079
+ """Sets a condition for records to be considered in agregate function
1080
+ operations.
1081
+
1082
+ Args:
1083
+ condition (string): A condition on the agregate functions used on
1084
+ the grouping criteria.
1085
+
1086
+ Returns:
1087
+ mysqlx.ReadStatement: ReadStatement object.
1088
+ """
1089
+ self._set_having(condition)
1090
+ return self
1091
+
1092
+ def execute(self) -> Union[DocResult, RowResult]:
1093
+ """Execute the statement.
1094
+
1095
+ Returns:
1096
+ mysqlx.Result: Result object.
1097
+ """
1098
+ return self._connection.send_find(self)
1099
+
1100
+
1101
+ class FindStatement(ReadStatement):
1102
+ """A statement document selection on a Collection.
1103
+
1104
+ Args:
1105
+ collection (mysqlx.Collection): The Collection object.
1106
+ condition (Optional[str]): An optional expression to identify the
1107
+ documents to be retrieved. If not specified
1108
+ all the documents will be included on the
1109
+ result unless a limit is set.
1110
+ """
1111
+
1112
+ def __init__(
1113
+ self, collection: DatabaseTargetType, condition: Optional[str] = None
1114
+ ) -> None:
1115
+ super().__init__(collection, True, condition)
1116
+
1117
+ def fields(self, *fields: str) -> FindStatement:
1118
+ """Sets a document field filter.
1119
+
1120
+ Args:
1121
+ *fields: The string expressions identifying the fields to be
1122
+ extracted.
1123
+
1124
+ Returns:
1125
+ mysqlx.FindStatement: FindStatement object.
1126
+ """
1127
+ return self._set_projection(*fields)
1128
+
1129
+ def sort(self, *clauses: str) -> FindStatement:
1130
+ """Sets the sorting criteria.
1131
+
1132
+ Args:
1133
+ *clauses: The expression strings defining the sort criteria.
1134
+
1135
+ Returns:
1136
+ mysqlx.FindStatement: FindStatement object.
1137
+ """
1138
+ return self._sort(*clauses)
1139
+
1140
+
1141
+ class SelectStatement(ReadStatement):
1142
+ """A statement for record retrieval operations on a Table.
1143
+
1144
+ Args:
1145
+ table (mysqlx.Table): The Table object.
1146
+ *fields: The fields to be retrieved.
1147
+ """
1148
+
1149
+ def __init__(self, table: DatabaseTargetType, *fields: str) -> None:
1150
+ super().__init__(table, False)
1151
+ self._set_projection(*fields)
1152
+
1153
+ def where(self, condition: str) -> SelectStatement:
1154
+ """Sets the search condition to filter.
1155
+
1156
+ Args:
1157
+ condition (str): Sets the search condition to filter records.
1158
+
1159
+ Returns:
1160
+ mysqlx.SelectStatement: SelectStatement object.
1161
+ """
1162
+ return self._set_where(condition)
1163
+
1164
+ def order_by(self, *clauses: str) -> SelectStatement:
1165
+ """Sets the order by criteria.
1166
+
1167
+ Args:
1168
+ *clauses: The expression strings defining the order by criteria.
1169
+
1170
+ Returns:
1171
+ mysqlx.SelectStatement: SelectStatement object.
1172
+ """
1173
+ return self._sort(*clauses)
1174
+
1175
+ def get_sql(self) -> str:
1176
+ """Returns the generated SQL.
1177
+
1178
+ Returns:
1179
+ str: The generated SQL.
1180
+ """
1181
+ where = f" WHERE {self._where_str}" if self.has_where else ""
1182
+ group_by = f" GROUP BY {self._grouping_str}" if self.has_group_by else ""
1183
+ having = f" HAVING {self._having}" if self.has_having else ""
1184
+ order_by = f" ORDER BY {self._sort_str}" if self.has_sort else ""
1185
+ limit = (
1186
+ f" LIMIT {self._limit_row_count} OFFSET {self._limit_offset}"
1187
+ if self.has_limit
1188
+ else ""
1189
+ )
1190
+ stmt = (
1191
+ f"SELECT {self._projection_str or '*'} "
1192
+ f"FROM {self.schema.name}.{self.target.name}"
1193
+ f"{where}{group_by}{having}{order_by}{limit}"
1194
+ )
1195
+ return stmt
1196
+
1197
+
1198
+ class InsertStatement(WriteStatement):
1199
+ """A statement for insert operations on Table.
1200
+
1201
+ Args:
1202
+ table (mysqlx.Table): The Table object.
1203
+ *fields: The fields to be inserted.
1204
+ """
1205
+
1206
+ def __init__(self, table: DatabaseTargetType, *fields: Any) -> None:
1207
+ super().__init__(table, False)
1208
+ self._fields: Union[List, Tuple] = flexible_params(*fields)
1209
+
1210
+ def values(self, *values: Any) -> InsertStatement:
1211
+ """Set the values to be inserted.
1212
+
1213
+ Args:
1214
+ *values: The values of the columns to be inserted.
1215
+
1216
+ Returns:
1217
+ mysqlx.InsertStatement: InsertStatement object.
1218
+ """
1219
+ self._values.append(list(flexible_params(*values)))
1220
+ return self
1221
+
1222
+ def execute(self) -> Result:
1223
+ """Execute the statement.
1224
+
1225
+ Returns:
1226
+ mysqlx.Result: Result object.
1227
+ """
1228
+ return self._connection.send_insert(self)
1229
+
1230
+
1231
+ class UpdateStatement(FilterableStatement):
1232
+ """A statement for record update operations on a Table.
1233
+
1234
+ Args:
1235
+ table (mysqlx.Table): The Table object.
1236
+
1237
+ .. versionchanged:: 8.0.12
1238
+ The ``fields`` parameters were removed.
1239
+ """
1240
+
1241
+ def __init__(self, table: DatabaseTargetType) -> None:
1242
+ super().__init__(target=table, doc_based=False)
1243
+ self._update_ops: Dict[str, Any] = {}
1244
+
1245
+ def where(self, condition: str) -> UpdateStatement:
1246
+ """Sets the search condition to filter.
1247
+
1248
+ Args:
1249
+ condition (str): Sets the search condition to filter records.
1250
+
1251
+ Returns:
1252
+ mysqlx.UpdateStatement: UpdateStatement object.
1253
+ """
1254
+ return self._set_where(condition)
1255
+
1256
+ def order_by(self, *clauses: str) -> UpdateStatement:
1257
+ """Sets the order by criteria.
1258
+
1259
+ Args:
1260
+ *clauses: The expression strings defining the order by criteria.
1261
+
1262
+ Returns:
1263
+ mysqlx.UpdateStatement: UpdateStatement object.
1264
+ """
1265
+ return self._sort(*clauses)
1266
+
1267
+ def get_update_ops(self) -> Dict[str, Any]:
1268
+ """Returns the list of update operations.
1269
+
1270
+ Returns:
1271
+ `list`: The list of update operations.
1272
+ """
1273
+ return self._update_ops
1274
+
1275
+ def set(self, field: str, value: Any) -> UpdateStatement:
1276
+ """Updates the column value on records in a table.
1277
+
1278
+ Args:
1279
+ field (string): The column name to be updated.
1280
+ value (object): The value to be set on the specified column.
1281
+
1282
+ Returns:
1283
+ mysqlx.UpdateStatement: UpdateStatement object.
1284
+ """
1285
+ self._update_ops[field] = UpdateSpec(
1286
+ mysqlxpb_enum("Mysqlx.Crud.UpdateOperation.UpdateType.SET"),
1287
+ field,
1288
+ value,
1289
+ )
1290
+ self._changed = True
1291
+ return self
1292
+
1293
+ def execute(self) -> Result:
1294
+ """Execute the statement.
1295
+
1296
+ Returns:
1297
+ mysqlx.Result: Result object
1298
+
1299
+ Raises:
1300
+ ProgrammingError: If condition was not set.
1301
+ """
1302
+ if not self.has_where:
1303
+ raise ProgrammingError("No condition was found for update")
1304
+ return self._connection.send_update(self)
1305
+
1306
+
1307
+ class RemoveStatement(FilterableStatement):
1308
+ """A statement for document removal from a collection.
1309
+
1310
+ Args:
1311
+ collection (mysqlx.Collection): The Collection object.
1312
+ condition (str): Sets the search condition to identify the documents
1313
+ to be removed.
1314
+
1315
+ .. versionchanged:: 8.0.12
1316
+ The ``condition`` parameter was added.
1317
+ """
1318
+
1319
+ def __init__(self, collection: DatabaseTargetType, condition: str) -> None:
1320
+ super().__init__(target=collection, condition=condition)
1321
+
1322
+ def sort(self, *clauses: str) -> RemoveStatement:
1323
+ """Sets the sorting criteria.
1324
+
1325
+ Args:
1326
+ *clauses: The expression strings defining the sort criteria.
1327
+
1328
+ Returns:
1329
+ mysqlx.FindStatement: FindStatement object.
1330
+ """
1331
+ return self._sort(*clauses)
1332
+
1333
+ def execute(self) -> Result:
1334
+ """Execute the statement.
1335
+
1336
+ Returns:
1337
+ mysqlx.Result: Result object.
1338
+
1339
+ Raises:
1340
+ ProgrammingError: If condition was not set.
1341
+ """
1342
+ if not self.has_where:
1343
+ raise ProgrammingError("No condition was found for remove")
1344
+ return self._connection.send_delete(self)
1345
+
1346
+
1347
+ class DeleteStatement(FilterableStatement):
1348
+ """A statement that drops a table.
1349
+
1350
+ Args:
1351
+ table (mysqlx.Table): The Table object.
1352
+
1353
+ .. versionchanged:: 8.0.12
1354
+ The ``condition`` parameter was removed.
1355
+ """
1356
+
1357
+ def __init__(self, table: DatabaseTargetType) -> None:
1358
+ super().__init__(target=table, doc_based=False)
1359
+
1360
+ def where(self, condition: str) -> DeleteStatement:
1361
+ """Sets the search condition to filter.
1362
+
1363
+ Args:
1364
+ condition (str): Sets the search condition to filter records.
1365
+
1366
+ Returns:
1367
+ mysqlx.DeleteStatement: DeleteStatement object.
1368
+ """
1369
+ return self._set_where(condition)
1370
+
1371
+ def order_by(self, *clauses: str) -> DeleteStatement:
1372
+ """Sets the order by criteria.
1373
+
1374
+ Args:
1375
+ *clauses: The expression strings defining the order by criteria.
1376
+
1377
+ Returns:
1378
+ mysqlx.DeleteStatement: DeleteStatement object.
1379
+ """
1380
+ return self._sort(*clauses)
1381
+
1382
+ def execute(self) -> Result:
1383
+ """Execute the statement.
1384
+
1385
+ Returns:
1386
+ mysqlx.Result: Result object.
1387
+
1388
+ Raises:
1389
+ ProgrammingError: If condition was not set.
1390
+ """
1391
+ if not self.has_where:
1392
+ raise ProgrammingError("No condition was found for delete")
1393
+ return self._connection.send_delete(self)
1394
+
1395
+
1396
+ class CreateCollectionIndexStatement(Statement):
1397
+ """A statement that creates an index on a collection.
1398
+
1399
+ Args:
1400
+ collection (mysqlx.Collection): Collection.
1401
+ index_name (string): Index name.
1402
+ index_desc (dict): A dictionary containing the fields members that
1403
+ constraints the index to be created. It must have
1404
+ the form as shown in the following::
1405
+
1406
+ {"fields": [{"field": member_path,
1407
+ "type": member_type,
1408
+ "required": member_required,
1409
+ "collation": collation,
1410
+ "options": options,
1411
+ "srid": srid},
1412
+ # {... more members,
1413
+ # repeated as many times
1414
+ # as needed}
1415
+ ],
1416
+ "type": type}
1417
+ """
1418
+
1419
+ def __init__(
1420
+ self,
1421
+ collection: DatabaseTargetType,
1422
+ index_name: str,
1423
+ index_desc: Dict[str, Any],
1424
+ ) -> None:
1425
+ super().__init__(target=collection)
1426
+ self._index_desc: Dict[str, Any] = copy.deepcopy(index_desc)
1427
+ self._index_name: str = index_name
1428
+ self._fields_desc: List[Dict[str, Any]] = self._index_desc.pop("fields", [])
1429
+
1430
+ def execute(self) -> Result:
1431
+ """Execute the statement.
1432
+
1433
+ Returns:
1434
+ mysqlx.Result: Result object.
1435
+ """
1436
+ # Validate index name is a valid identifier
1437
+ if self._index_name is None:
1438
+ raise ProgrammingError(ERR_INVALID_INDEX_NAME.format(self._index_name))
1439
+ try:
1440
+ parsed_ident = ExprParser(self._index_name).expr().get_message()
1441
+
1442
+ # The message is type dict when the Protobuf cext is used
1443
+ if isinstance(parsed_ident, dict):
1444
+ if parsed_ident["type"] != mysqlxpb_enum("Mysqlx.Expr.Expr.Type.IDENT"):
1445
+ raise ProgrammingError(
1446
+ ERR_INVALID_INDEX_NAME.format(self._index_name)
1447
+ )
1448
+ else:
1449
+ if parsed_ident.type != mysqlxpb_enum("Mysqlx.Expr.Expr.Type.IDENT"):
1450
+ raise ProgrammingError(
1451
+ ERR_INVALID_INDEX_NAME.format(self._index_name)
1452
+ )
1453
+
1454
+ except (ValueError, AttributeError) as err:
1455
+ raise ProgrammingError(
1456
+ ERR_INVALID_INDEX_NAME.format(self._index_name)
1457
+ ) from err
1458
+
1459
+ # Validate members that constraint the index
1460
+ if not self._fields_desc:
1461
+ raise ProgrammingError(
1462
+ "Required member 'fields' not found in the given index "
1463
+ f"description: {self._index_desc}"
1464
+ )
1465
+
1466
+ if not isinstance(self._fields_desc, list):
1467
+ raise ProgrammingError("Required member 'fields' must contain a list")
1468
+ args: Dict[str, Any] = {}
1469
+ args["name"] = self._index_name
1470
+ args["collection"] = self._target.name
1471
+ args["schema"] = self._target.schema.name
1472
+ if "type" in self._index_desc:
1473
+ args["type"] = self._index_desc.pop("type")
1474
+ else:
1475
+ args["type"] = "INDEX"
1476
+ args["unique"] = self._index_desc.pop("unique", False)
1477
+ # Currently unique indexes are not supported:
1478
+ if args["unique"]:
1479
+ raise NotSupportedError("Unique indexes are not supported.")
1480
+ args["constraint"] = []
1481
+
1482
+ if self._index_desc:
1483
+ raise ProgrammingError(f"Unidentified fields: {self._index_desc}")
1484
+
1485
+ try:
1486
+ for field_desc in self._fields_desc:
1487
+ constraint = {}
1488
+ constraint["member"] = field_desc.pop("field")
1489
+ constraint["type"] = field_desc.pop("type")
1490
+ constraint["required"] = field_desc.pop("required", False)
1491
+ constraint["array"] = field_desc.pop("array", False)
1492
+ if not isinstance(constraint["required"], bool):
1493
+ raise TypeError("Field member 'required' must be Boolean")
1494
+ if not isinstance(constraint["array"], bool):
1495
+ raise TypeError("Field member 'array' must be Boolean")
1496
+ if args["type"].upper() == "SPATIAL" and not constraint["required"]:
1497
+ raise ProgrammingError(
1498
+ "Field member 'required' must be set to 'True' when "
1499
+ "index type is set to 'SPATIAL'"
1500
+ )
1501
+ if args["type"].upper() == "INDEX" and constraint["type"] == "GEOJSON":
1502
+ raise ProgrammingError(
1503
+ "Index 'type' must be set to 'SPATIAL' when field "
1504
+ "type is set to 'GEOJSON'"
1505
+ )
1506
+ if "collation" in field_desc:
1507
+ if not constraint["type"].upper().startswith("TEXT"):
1508
+ raise ProgrammingError(
1509
+ "The 'collation' member can only be used when "
1510
+ "field type is set to "
1511
+ f"'{constraint['type'].upper()}'"
1512
+ )
1513
+ constraint["collation"] = field_desc.pop("collation")
1514
+ # "options" and "srid" fields in IndexField can be
1515
+ # present only if "type" is set to "GEOJSON"
1516
+ if "options" in field_desc:
1517
+ if constraint["type"].upper() != "GEOJSON":
1518
+ raise ProgrammingError(
1519
+ "The 'options' member can only be used when "
1520
+ "index type is set to 'GEOJSON'"
1521
+ )
1522
+ constraint["options"] = field_desc.pop("options")
1523
+ if "srid" in field_desc:
1524
+ if constraint["type"].upper() != "GEOJSON":
1525
+ raise ProgrammingError(
1526
+ "The 'srid' member can only be used when index "
1527
+ "type is set to 'GEOJSON'"
1528
+ )
1529
+ constraint["srid"] = field_desc.pop("srid")
1530
+ args["constraint"].append(constraint)
1531
+ except KeyError as err:
1532
+ raise ProgrammingError(
1533
+ f"Required inner member {err} not found in constraint: {field_desc}"
1534
+ ) from err
1535
+
1536
+ for field_desc in self._fields_desc:
1537
+ if field_desc:
1538
+ raise ProgrammingError(f"Unidentified inner fields: {field_desc}")
1539
+
1540
+ return self._connection.execute_nonquery(
1541
+ "mysqlx", "create_collection_index", True, args
1542
+ )