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.
- DefPixel/DataAccessObject/Implements/MariaDB/Column.py +39 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Compiler.py +84 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Configuration.py +98 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Connection.py +176 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Constraints/__init__.py +111 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Context.py +69 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Cursor.py +137 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Create.py +290 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/DDL.py +50 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Delete.py +38 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Drop.py +43 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Get.py +10 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Insert.py +46 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Query.py +100 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Select.py +208 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Union.py +72 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/Update.py +50 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/With.py +103 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Executors/__init__.py +27 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Expr.py +261 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Binary.py +58 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Condition.py +119 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Operator.py +88 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Exprs/Subquery.py +38 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Exprs/__init__.py +35 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Function.py +42 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Address.py +27 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Aggregate.py +184 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Control.py +157 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/CurrentTimestamp.py +20 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/DateTime.py +262 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Identity.py +13 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/JSON.py +273 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Numeric.py +123 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Sequence.py +82 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Spatial.py +141 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/String.py +184 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Vector.py +29 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/Window.py +489 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Functions/__init__.py +443 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Index.py +72 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Joins/__init__.py +111 -0
- DefPixel/DataAccessObject/Implements/MariaDB/JsonTable.py +236 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Mapping.py +330 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Pool.py +104 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Run.py +76 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Sequence.py +131 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Session.py +105 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Source.py +126 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Type.py +130 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Binary.py +82 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Boolean.py +69 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/DateTime.py +158 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Enumeration.py +114 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Integer.py +79 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/JSON.py +57 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Native.py +69 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Numeric.py +151 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Serial.py +21 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Spatial.py +72 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Text.py +113 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/Vector.py +47 -0
- DefPixel/DataAccessObject/Implements/MariaDB/Types/__init__.py +262 -0
- DefPixel/DataAccessObject/Implements/MariaDB/__init__.py +65 -0
- defpixel_mariadb-0.1.6.dist-info/METADATA +184 -0
- defpixel_mariadb-0.1.6.dist-info/RECORD +69 -0
- defpixel_mariadb-0.1.6.dist-info/WHEEL +6 -0
- defpixel_mariadb-0.1.6.dist-info/licenses/LICENSE +674 -0
- 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
|