DefPixel-MariaDB 0.1.6__91-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.
Files changed (69) hide show
  1. DefPixel/DataAccessObject/Implements/MariaDB/Column.py +39 -0
  2. DefPixel/DataAccessObject/Implements/MariaDB/Compiler.py +84 -0
  3. DefPixel/DataAccessObject/Implements/MariaDB/Configuration.py +98 -0
  4. DefPixel/DataAccessObject/Implements/MariaDB/Connection.py +176 -0
  5. DefPixel/DataAccessObject/Implements/MariaDB/Constraints/__init__.py +111 -0
  6. DefPixel/DataAccessObject/Implements/MariaDB/Context.py +69 -0
  7. DefPixel/DataAccessObject/Implements/MariaDB/Cursor.py +137 -0
  8. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Create.py +290 -0
  9. DefPixel/DataAccessObject/Implements/MariaDB/Executors/DDL.py +50 -0
  10. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Delete.py +38 -0
  11. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Drop.py +43 -0
  12. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Get.py +10 -0
  13. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Insert.py +46 -0
  14. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Query.py +100 -0
  15. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Select.py +208 -0
  16. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Union.py +72 -0
  17. DefPixel/DataAccessObject/Implements/MariaDB/Executors/Update.py +50 -0
  18. DefPixel/DataAccessObject/Implements/MariaDB/Executors/With.py +103 -0
  19. DefPixel/DataAccessObject/Implements/MariaDB/Executors/__init__.py +27 -0
  20. DefPixel/DataAccessObject/Implements/MariaDB/Expr.py +261 -0
  21. DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Binary.py +58 -0
  22. DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Condition.py +119 -0
  23. DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Operator.py +88 -0
  24. DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Subquery.py +38 -0
  25. DefPixel/DataAccessObject/Implements/MariaDB/Exprs/__init__.py +35 -0
  26. DefPixel/DataAccessObject/Implements/MariaDB/Function.py +42 -0
  27. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Address.py +27 -0
  28. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Aggregate.py +184 -0
  29. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Control.py +157 -0
  30. DefPixel/DataAccessObject/Implements/MariaDB/Functions/CurrentTimestamp.py +20 -0
  31. DefPixel/DataAccessObject/Implements/MariaDB/Functions/DateTime.py +262 -0
  32. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Identity.py +13 -0
  33. DefPixel/DataAccessObject/Implements/MariaDB/Functions/JSON.py +273 -0
  34. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Numeric.py +123 -0
  35. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Sequence.py +82 -0
  36. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Spatial.py +141 -0
  37. DefPixel/DataAccessObject/Implements/MariaDB/Functions/String.py +184 -0
  38. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Vector.py +29 -0
  39. DefPixel/DataAccessObject/Implements/MariaDB/Functions/Window.py +489 -0
  40. DefPixel/DataAccessObject/Implements/MariaDB/Functions/__init__.py +443 -0
  41. DefPixel/DataAccessObject/Implements/MariaDB/Index.py +72 -0
  42. DefPixel/DataAccessObject/Implements/MariaDB/Joins/__init__.py +111 -0
  43. DefPixel/DataAccessObject/Implements/MariaDB/JsonTable.py +236 -0
  44. DefPixel/DataAccessObject/Implements/MariaDB/Mapping.py +330 -0
  45. DefPixel/DataAccessObject/Implements/MariaDB/Pool.py +104 -0
  46. DefPixel/DataAccessObject/Implements/MariaDB/Run.py +76 -0
  47. DefPixel/DataAccessObject/Implements/MariaDB/Sequence.py +131 -0
  48. DefPixel/DataAccessObject/Implements/MariaDB/Session.py +105 -0
  49. DefPixel/DataAccessObject/Implements/MariaDB/Source.py +126 -0
  50. DefPixel/DataAccessObject/Implements/MariaDB/Type.py +130 -0
  51. DefPixel/DataAccessObject/Implements/MariaDB/Types/Binary.py +82 -0
  52. DefPixel/DataAccessObject/Implements/MariaDB/Types/Boolean.py +69 -0
  53. DefPixel/DataAccessObject/Implements/MariaDB/Types/DateTime.py +158 -0
  54. DefPixel/DataAccessObject/Implements/MariaDB/Types/Enumeration.py +114 -0
  55. DefPixel/DataAccessObject/Implements/MariaDB/Types/Integer.py +79 -0
  56. DefPixel/DataAccessObject/Implements/MariaDB/Types/JSON.py +57 -0
  57. DefPixel/DataAccessObject/Implements/MariaDB/Types/Native.py +69 -0
  58. DefPixel/DataAccessObject/Implements/MariaDB/Types/Numeric.py +151 -0
  59. DefPixel/DataAccessObject/Implements/MariaDB/Types/Serial.py +21 -0
  60. DefPixel/DataAccessObject/Implements/MariaDB/Types/Spatial.py +72 -0
  61. DefPixel/DataAccessObject/Implements/MariaDB/Types/Text.py +113 -0
  62. DefPixel/DataAccessObject/Implements/MariaDB/Types/Vector.py +47 -0
  63. DefPixel/DataAccessObject/Implements/MariaDB/Types/__init__.py +262 -0
  64. DefPixel/DataAccessObject/Implements/MariaDB/__init__.py +65 -0
  65. defpixel_mariadb-0.1.6.dist-info/METADATA +184 -0
  66. defpixel_mariadb-0.1.6.dist-info/RECORD +69 -0
  67. defpixel_mariadb-0.1.6.dist-info/WHEEL +6 -0
  68. defpixel_mariadb-0.1.6.dist-info/licenses/LICENSE +674 -0
  69. defpixel_mariadb-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from .Compiler import (
6
+ Compiler,
7
+ identifier,
8
+ qualified,
9
+ )
10
+ from .Expr import Expr
11
+
12
+
13
+ class Column(Expr):
14
+ """
15
+ An explicitly named SQL column with an optional table/alias and database.
16
+ """
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ *,
21
+ table: Optional[str] = None,
22
+ database: Optional[str] = None,
23
+ ) -> None:
24
+ identifier(name)
25
+ if table is not None:
26
+ identifier(table)
27
+ if database is not None:
28
+ identifier(database)
29
+ if table is None:
30
+ raise ValueError('A qualified database column requires a table')
31
+ self.name = name
32
+ self.table = table
33
+ self.database = database
34
+ return
35
+
36
+ def compile(self, compiler: Compiler) -> str:
37
+ if self.table is None or not compiler.qualify:
38
+ return identifier(self.name)
39
+ return '{}.{}'.format(qualified(self.table, self.database), identifier(self.name))
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABC,
5
+ abstractmethod,
6
+ )
7
+ from decimal import Decimal
8
+ from math import isfinite
9
+ from typing import Optional
10
+
11
+
12
+ def identifier(name: str) -> str:
13
+ """
14
+ Quote a MariaDB identifier without changing percent signs in qmark SQL.
15
+ """
16
+ if not isinstance(name, str) or not name or '\x00' in name:
17
+ raise ValueError('SQL identifiers must be nonempty strings without null characters')
18
+ return '`{}`'.format(name.replace('`', '``'))
19
+
20
+
21
+ def qualified(
22
+ name: str,
23
+ database: Optional[str] = None,
24
+ ) -> str:
25
+ if database is None:
26
+ return identifier(name)
27
+ return '{}.{}'.format(identifier(database), identifier(name))
28
+
29
+
30
+ class Compilable(ABC):
31
+ """
32
+ A MariaDB SQL object compiled using one shared parameter collector.
33
+ """
34
+ @abstractmethod
35
+ def compile(self, compiler: 'Compiler') -> str:
36
+ raise NotImplementedError
37
+
38
+ def __str__(self) -> str:
39
+ return self.compile(Compiler())
40
+
41
+
42
+ class Compiler:
43
+ """
44
+ Collect qmark parameters, or render SQL-mode-independent DDL literals.
45
+ """
46
+ def __init__(
47
+ self,
48
+ literal: bool = False,
49
+ qualify: bool = True,
50
+ ) -> None:
51
+ if type(literal) is not bool or type(qualify) is not bool:
52
+ raise TypeError('Compiler flags must be bool values')
53
+ self.literal = literal
54
+ self.qualify = qualify
55
+ self.args: list[object] = []
56
+ return
57
+
58
+ def bind(self, value: object) -> str:
59
+ if isinstance(value, Compilable):
60
+ raise TypeError('Compilable objects must be compiled, not bound')
61
+ if not self.literal:
62
+ self.args.append(value)
63
+ return '?'
64
+ if value is None:
65
+ return 'NULL'
66
+ if isinstance(value, bool):
67
+ return str(int(value))
68
+ if isinstance(value, int):
69
+ return str(value)
70
+ if isinstance(value, (float, Decimal)):
71
+ if not (value.is_finite() if isinstance(value, Decimal) else isfinite(value)):
72
+ raise ValueError('SQL numeric literals must be finite')
73
+ return str(value)
74
+ if isinstance(value, bytes):
75
+ return "X'{}'".format(value.hex())
76
+ if isinstance(value, str):
77
+ # Hex text avoids quoting differences under NO_BACKSLASH_ESCAPES.
78
+ return "CONVERT(X'{}' USING utf8mb4)".format(value.encode('utf8').hex())
79
+ raise TypeError('Unsupported MariaDB literal type: {}'.format(type(value).__name__))
80
+
81
+ def compile(self, value: object) -> str:
82
+ if isinstance(value, Compilable):
83
+ return value.compile(self)
84
+ return self.bind(value)
@@ -0,0 +1,98 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from DefPixel.DataAccessObject import Configuration as BaseConfiguration
6
+
7
+ __all__ = (
8
+ 'Configuration',
9
+ )
10
+
11
+
12
+ def text(
13
+ name: str,
14
+ value: Optional[str],
15
+ required: bool,
16
+ ) -> Optional[str]:
17
+ if value is None and not required:
18
+ return None
19
+ if not isinstance(value, str) or not value or '\x00' in value:
20
+ raise ValueError('{} must be a nonempty string without null characters'.format(name))
21
+ return value
22
+
23
+
24
+ def integer(
25
+ name: str,
26
+ value: int,
27
+ minimum: int,
28
+ maximum: int,
29
+ ) -> int:
30
+ if type(value) is not int:
31
+ raise TypeError('{} must be an integer'.format(name))
32
+ if not minimum <= value <= maximum:
33
+ raise ValueError('{} must be between {} and {}'.format(name, minimum, maximum))
34
+ return value
35
+
36
+
37
+ def flag(
38
+ name: str,
39
+ value: bool,
40
+ ) -> bool:
41
+ if type(value) is not bool:
42
+ raise TypeError('{} must be a bool'.format(name))
43
+ return value
44
+
45
+
46
+ class Configuration(BaseConfiguration):
47
+ """
48
+ MariaDB connection settings and an optional fixed-size native pool.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ host: str,
54
+ port: int = 3306,
55
+ database: Optional[str] = None,
56
+ username: Optional[str] = None,
57
+ password: Optional[str] = None,
58
+ autocommit: bool = False,
59
+ pool: bool = False,
60
+ pool_size: int = 5,
61
+ timezone: Optional[str] = None,
62
+ ) -> None:
63
+ self.host = text('host', host, True)
64
+ self.port = integer('port', port, 1, 65535)
65
+ self.database = text('database', database, True)
66
+ self.username = text('username', username, False)
67
+ self.password = text('password', password, False)
68
+ self.autocommit = flag('autocommit', autocommit)
69
+ self.pool = flag('pool', pool)
70
+ self.pool_size = integer('pool_size', pool_size, 1, 64)
71
+ self.timezone = text('timezone', timezone, False)
72
+ return
73
+
74
+ def parameters(self) -> dict[str, object]:
75
+ result: dict[str, object] = {
76
+ 'host': self.host,
77
+ 'port': self.port,
78
+ 'database': self.database,
79
+ 'autocommit': self.autocommit,
80
+ }
81
+ if self.username is not None:
82
+ result['user'] = self.username
83
+ if self.password is not None:
84
+ result['password'] = self.password
85
+ return result
86
+
87
+ def __repr__(self) -> str:
88
+ return '{}(host={!r}, port={!r}, database={!r}, username={!r}, autocommit={!r}, pool={!r}, pool_size={!r}, timezone={!r})'.format(
89
+ self.__class__.__name__,
90
+ self.host,
91
+ self.port,
92
+ self.database,
93
+ self.username,
94
+ self.autocommit,
95
+ self.pool,
96
+ self.pool_size,
97
+ self.timezone,
98
+ )
@@ -0,0 +1,176 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from itertools import count
4
+ from typing import (
5
+ Callable,
6
+ Optional,
7
+ )
8
+
9
+ from mariadb import (
10
+ Connection as NativeConnection,
11
+ connect,
12
+ )
13
+ from mariadb.constants.STATUS import IN_TRANS
14
+
15
+ from DefPixel.DataAccessObject import Connection as BaseConnection
16
+ from DefPixel.DataAccessObject.Properties.Database import Database
17
+ from .Configuration import Configuration
18
+ from .Context import Context
19
+ from .Cursor import (
20
+ Cursor,
21
+ ParameterSet,
22
+ )
23
+ from .Pool import Pool
24
+ from .Session import Session
25
+
26
+ __all__ = (
27
+ 'Connection',
28
+ )
29
+
30
+
31
+ class Connection(
32
+ BaseConnection,
33
+ Database,
34
+ ):
35
+ """
36
+ Explicit MariaDB lifecycle, buffered results, transactions and native pools.
37
+ """
38
+
39
+ def __init__(self, conf: Configuration) -> None:
40
+ if not isinstance(conf, Configuration):
41
+ raise TypeError('Connection requires a MariaDB Configuration')
42
+ self.conf = conf
43
+ self.connection: Optional[NativeConnection] = None
44
+ self.cursors: set[Cursor] = set()
45
+ self.savepoints = count()
46
+ return
47
+
48
+ def connect(self) -> None:
49
+ if self.connection is not None:
50
+ raise RuntimeError('The MariaDB connection is already open')
51
+ if self.conf.pool:
52
+ self.connection = Pool.get(self.conf)
53
+ return
54
+ connection: NativeConnection = connect(**self.conf.parameters())
55
+ try:
56
+ Pool.configure(connection, self.conf)
57
+ except BaseException:
58
+ connection.close()
59
+ raise
60
+ self.connection = connection
61
+ return
62
+
63
+ def active(self) -> NativeConnection:
64
+ if self.connection is None:
65
+ raise RuntimeError('The MariaDB connection is not open')
66
+ return self.connection
67
+
68
+ def close(self) -> None:
69
+ if self.connection is None:
70
+ return
71
+ connection: NativeConnection = self.connection
72
+ self.connection = None
73
+ errors: list[Exception] = []
74
+ for cursor in tuple(self.cursors):
75
+ try:
76
+ cursor.close()
77
+ except Exception as error:
78
+ errors.append(error)
79
+ try:
80
+ if self.conf.pool:
81
+ Pool.release(self.conf, connection)
82
+ else:
83
+ try:
84
+ if connection.server_status & IN_TRANS:
85
+ connection.rollback()
86
+ finally:
87
+ connection.close()
88
+ except Exception as error:
89
+ errors.append(error)
90
+ if errors:
91
+ raise RuntimeError('Failed to close the MariaDB connection or its cursors') from errors[0]
92
+ return
93
+
94
+ def autocommit(self, operation: str) -> NativeConnection:
95
+ if self.conf.autocommit:
96
+ raise RuntimeError('{} is not available in autocommit mode; use session() for a transaction'.format(operation))
97
+ return self.active()
98
+
99
+ def begin(self) -> None:
100
+ connection: NativeConnection = self.autocommit('begin')
101
+ if connection.server_status & IN_TRANS:
102
+ raise RuntimeError('A transaction is already in progress')
103
+ with self.execute('START TRANSACTION'):
104
+ pass
105
+ return
106
+
107
+ def execute(
108
+ self,
109
+ sql: str,
110
+ *args: object,
111
+ **kwargs: object,
112
+ ) -> Context:
113
+ cursor: Cursor = self.cursor()
114
+ try:
115
+ return cursor.execute(sql, *args, **kwargs)
116
+ except BaseException:
117
+ cursor.close()
118
+ raise
119
+
120
+ def executes(
121
+ self,
122
+ sql: str,
123
+ *args: ParameterSet,
124
+ ) -> Context:
125
+ cursor: Cursor = self.cursor()
126
+ try:
127
+ return cursor.executes(sql, *args)
128
+ except BaseException:
129
+ cursor.close()
130
+ raise
131
+
132
+ def cursor(self) -> Cursor:
133
+ cursor = Cursor(
134
+ self.active().cursor(dictionary=True, buffered=True),
135
+ self.cursors.discard,
136
+ )
137
+ self.cursors.add(cursor)
138
+ return cursor
139
+
140
+ def run(
141
+ self,
142
+ executor: object,
143
+ filter: Optional[Callable[[dict[str, object]], dict[str, object]]] = None,
144
+ fetch: Optional[type] = None,
145
+ ) -> Optional[object | list[object]]:
146
+ """
147
+ Execute model SQL/DDL on a dedicated cursor and release it on every outcome.
148
+ """
149
+ cursor: Cursor = self.cursor()
150
+ try:
151
+ return cursor.run(
152
+ executor,
153
+ filter=filter,
154
+ fetch=fetch,
155
+ )
156
+ finally:
157
+ cursor.close()
158
+
159
+ def session(self) -> Session:
160
+ connection: NativeConnection = self.active()
161
+ session = Session(
162
+ connection,
163
+ connection.cursor(dictionary=True, buffered=True),
164
+ lambda: 'defpixel_savepoint_{}'.format(next(self.savepoints)),
165
+ self.cursors.discard,
166
+ )
167
+ self.cursors.add(session)
168
+ return session
169
+
170
+ def commit(self) -> None:
171
+ self.autocommit('commit').commit()
172
+ return
173
+
174
+ def rollback(self) -> None:
175
+ self.autocommit('rollback').rollback()
176
+ return
@@ -0,0 +1,111 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Optional
5
+
6
+ from ..Compiler import (
7
+ Compilable,
8
+ identifier,
9
+ )
10
+
11
+ __all__ = (
12
+ 'Constraint',
13
+ 'PrimaryKey',
14
+ 'Unique',
15
+ 'ForeignKey',
16
+ 'Check',
17
+ )
18
+
19
+
20
+ def columns(value: Sequence[str]) -> tuple[str, ...]:
21
+ if isinstance(value, str) or not isinstance(value, Sequence) or not value:
22
+ raise ValueError('Constraint columns must be a nonempty sequence of names')
23
+ for name in value:
24
+ identifier(name)
25
+ if len(set(value)) != len(value):
26
+ raise ValueError('Constraint columns must not repeat')
27
+ return tuple(value)
28
+
29
+
30
+ class Constraint:
31
+ """
32
+ A named MariaDB table constraint.
33
+ """
34
+ def __init__(self, name: str) -> None:
35
+ identifier(name)
36
+ self.name = name
37
+ return
38
+
39
+
40
+ class Columns(Constraint):
41
+ def __init__(
42
+ self,
43
+ name: str,
44
+ cols: Sequence[str],
45
+ ) -> None:
46
+ super().__init__(name)
47
+ self.cols = columns(cols)
48
+ return
49
+
50
+
51
+ class PrimaryKey(Columns):
52
+ pass
53
+
54
+
55
+ class Unique(Columns):
56
+ pass
57
+
58
+
59
+ class ForeignKey(Columns):
60
+ """
61
+ Reference a mapped table or an explicit table name using InnoDB actions.
62
+ """
63
+ def __init__(
64
+ self,
65
+ name: str,
66
+ cols: Sequence[str],
67
+ reference: str | type,
68
+ reference_cols: Sequence[str],
69
+ *,
70
+ on_delete: Optional[str] = None,
71
+ on_update: Optional[str] = None,
72
+ ) -> None:
73
+ super().__init__(
74
+ name,
75
+ cols,
76
+ )
77
+ if isinstance(reference, str):
78
+ identifier(reference)
79
+ elif not isinstance(reference, type):
80
+ raise TypeError('Foreign key reference must be a mapped model or table name')
81
+ self.reference = reference
82
+ self.reference_cols = columns(reference_cols)
83
+ if len(self.cols) != len(self.reference_cols):
84
+ raise ValueError('Foreign key column counts must match')
85
+ for value in (
86
+ on_delete,
87
+ on_update,
88
+ ):
89
+ if value is not None and value not in (
90
+ 'RESTRICT',
91
+ 'NO ACTION',
92
+ 'CASCADE',
93
+ 'SET NULL',
94
+ ):
95
+ raise ValueError('Unsupported InnoDB foreign key action')
96
+ self.on_delete = on_delete
97
+ self.on_update = on_update
98
+ return
99
+
100
+
101
+ class Check(Constraint):
102
+ def __init__(
103
+ self,
104
+ name: str,
105
+ expression: Compilable,
106
+ ) -> None:
107
+ super().__init__(name)
108
+ if not isinstance(expression, Compilable):
109
+ raise TypeError('CHECK requires a MariaDB expression')
110
+ self.expression = expression
111
+ return
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from types import TracebackType
4
+ from typing import (
5
+ Callable,
6
+ Optional,
7
+ )
8
+
9
+ from mariadb import Cursor as NativeCursor
10
+
11
+ from DefPixel.DataAccessObject.Properties.Database import Context as BaseContext
12
+
13
+ __all__ = (
14
+ 'Context',
15
+ )
16
+
17
+
18
+ class Context(BaseContext):
19
+ """
20
+ A buffered dictionary result; row reads advance its owning cursor.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ cursor: NativeCursor,
26
+ query: Optional[str] = None,
27
+ close: Optional[Callable[[], None]] = None,
28
+ ) -> None:
29
+ self.cursor = cursor
30
+ self.statement = query
31
+ self.release = close
32
+ return
33
+
34
+ def rows(self) -> list[dict[str, object]]:
35
+ if self.cursor.description is None:
36
+ return []
37
+ return [dict(row) for row in self.cursor.fetchall()]
38
+
39
+ def row(self) -> Optional[dict[str, object]]:
40
+ if self.cursor.description is None:
41
+ return None
42
+ row = self.cursor.fetchone()
43
+ return dict(row) if row is not None else None
44
+
45
+ def count(self) -> int:
46
+ return self.cursor.rowcount
47
+
48
+ @property
49
+ def query(self) -> str:
50
+ if self.statement is None:
51
+ raise RuntimeError('The executed query is not available in this context')
52
+ return self.statement
53
+
54
+ def close(self) -> None:
55
+ if self.release is not None:
56
+ self.release()
57
+ return
58
+
59
+ def __enter__(self) -> 'Context':
60
+ return self
61
+
62
+ def __exit__(
63
+ self,
64
+ exception_type: Optional[type[BaseException]],
65
+ exception: Optional[BaseException],
66
+ traceback: Optional[TracebackType],
67
+ ) -> bool:
68
+ self.close()
69
+ return False
@@ -0,0 +1,137 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections.abc import (
4
+ Mapping,
5
+ Sequence,
6
+ )
7
+ from typing import (
8
+ Callable,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from mariadb import Cursor as NativeCursor
14
+
15
+ from DefPixel.DataAccessObject.Properties.Database import Cursor as BaseCursor
16
+ from .Context import Context
17
+
18
+ Parameters = Union[tuple[object, ...], dict[str, object]]
19
+ ParameterSet = Union[Sequence[object], Mapping[str, object]]
20
+
21
+ __all__ = (
22
+ 'Cursor',
23
+ )
24
+
25
+
26
+ def parameters(
27
+ args: tuple[object, ...],
28
+ kwargs: dict[str, object],
29
+ ) -> Optional[Parameters]:
30
+ if args and kwargs:
31
+ raise ValueError('Positional and named parameters cannot be used together')
32
+ if kwargs:
33
+ return kwargs
34
+ return args if args else None
35
+
36
+
37
+ def batch(args: tuple[ParameterSet, ...]) -> list[Parameters]:
38
+ if not args:
39
+ raise ValueError('executes requires at least one parameter set')
40
+ result: list[Parameters] = []
41
+ kinds: set[str] = set()
42
+ for item in args:
43
+ if isinstance(item, Mapping):
44
+ if any(not isinstance(key, str) for key in item):
45
+ raise TypeError('Named parameter keys must be strings')
46
+ kinds.add('named')
47
+ result.append(dict(item))
48
+ elif isinstance(item, Sequence) and not isinstance(item, (str, bytes, bytearray)):
49
+ kinds.add('positional')
50
+ result.append(tuple(item))
51
+ else:
52
+ raise TypeError('Each parameter set must be a sequence or a mapping')
53
+ if len(kinds) != 1:
54
+ raise ValueError('Positional and named parameter sets cannot be mixed')
55
+ return result
56
+
57
+
58
+ class Cursor(BaseCursor):
59
+ """
60
+ A native dictionary cursor with explicit lifetime and parameter validation.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ cursor: NativeCursor,
66
+ close: Optional[Callable[['Cursor'], None]] = None,
67
+ ) -> None:
68
+ self.cursor = cursor
69
+ self.release = close
70
+ self.closed = False
71
+ return
72
+
73
+ def active(self) -> NativeCursor:
74
+ if self.closed:
75
+ raise RuntimeError('The MariaDB cursor is closed')
76
+ return self.cursor
77
+
78
+ def execute(
79
+ self,
80
+ sql: str,
81
+ *args: object,
82
+ **kwargs: object,
83
+ ) -> Context:
84
+ if not isinstance(sql, str) or not sql.strip() or '\x00' in sql:
85
+ raise ValueError('SQL must be a nonempty string without null characters')
86
+ cursor: NativeCursor = self.active()
87
+ cursor.execute(sql, parameters(args, kwargs))
88
+ return Context(cursor, sql, self.close)
89
+
90
+ def executes(
91
+ self,
92
+ sql: str,
93
+ *args: ParameterSet,
94
+ ) -> Context:
95
+ if not isinstance(sql, str) or not sql.strip() or '\x00' in sql:
96
+ raise ValueError('SQL must be a nonempty string without null characters')
97
+ cursor: NativeCursor = self.active()
98
+ cursor.executemany(sql, batch(args))
99
+ return Context(cursor, sql, self.close)
100
+
101
+ def rows(self) -> list[dict[str, object]]:
102
+ return Context(self.active()).rows()
103
+
104
+ def row(self) -> Optional[dict[str, object]]:
105
+ return Context(self.active()).row()
106
+
107
+ def count(self) -> int:
108
+ return self.active().rowcount
109
+
110
+ def run(
111
+ self,
112
+ executor: object,
113
+ filter: Optional[Callable[[dict[str, object]], dict[str, object]]] = None,
114
+ fetch: Optional[type] = None,
115
+ ) -> Optional[object | list[object]]:
116
+ """
117
+ Execute model SQL/DDL while keeping this cursor owned by the caller.
118
+ """
119
+ from .Run import run
120
+ self.active()
121
+ return run(
122
+ self,
123
+ executor,
124
+ filter=filter,
125
+ fetch=fetch,
126
+ )
127
+
128
+ def close(self) -> None:
129
+ if self.closed:
130
+ return
131
+ self.closed = True
132
+ try:
133
+ self.cursor.close()
134
+ finally:
135
+ if self.release is not None:
136
+ self.release(self)
137
+ return