flowmaticdb 1.0.0__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.
- flowmaticdb/__init__.py +27 -0
- flowmaticdb/_helpers.py +48 -0
- flowmaticdb/_query_with_params.py +67 -0
- flowmaticdb/adapters/__init__.py +11 -0
- flowmaticdb/adapters/_base.py +98 -0
- flowmaticdb/adapters/_mysql.py +190 -0
- flowmaticdb/adapters/_postgres.py +162 -0
- flowmaticdb/adapters/_sqlite.py +165 -0
- flowmaticdb/database/__init__.py +7 -0
- flowmaticdb/database/_abc.py +128 -0
- flowmaticdb/database/_database.py +112 -0
- flowmaticdb/database/_db.py +7 -0
- flowmaticdb/database/_table.py +106 -0
- flowmaticdb/dialects/__init__.py +13 -0
- flowmaticdb/dialects/_base.py +178 -0
- flowmaticdb/dialects/_mysql.py +266 -0
- flowmaticdb/dialects/_postgres.py +137 -0
- flowmaticdb/dialects/_sql_dialect.py +739 -0
- flowmaticdb/dialects/_sqlite.py +178 -0
- flowmaticdb/exceptions.py +17 -0
- flowmaticdb/migrations/__init__.py +7 -0
- flowmaticdb/migrations/_loader.py +62 -0
- flowmaticdb/migrations/_migration_abc.py +23 -0
- flowmaticdb/migrations/_migrator.py +102 -0
- flowmaticdb/migrations/_template.py +34 -0
- flowmaticdb/query/__init__.py +19 -0
- flowmaticdb/query/_alter_table.py +36 -0
- flowmaticdb/query/_condition.py +16 -0
- flowmaticdb/query/_condition_group.py +176 -0
- flowmaticdb/query/_condition_mixin.py +135 -0
- flowmaticdb/query/_create_table.py +35 -0
- flowmaticdb/query/_ddl_mixins.py +313 -0
- flowmaticdb/query/_delete.py +29 -0
- flowmaticdb/query/_drop_table.py +34 -0
- flowmaticdb/query/_having_mixin.py +257 -0
- flowmaticdb/query/_insert.py +29 -0
- flowmaticdb/query/_join.py +269 -0
- flowmaticdb/query/_joins_mixin.py +60 -0
- flowmaticdb/query/_on_conflict.py +10 -0
- flowmaticdb/query/_order_by.py +11 -0
- flowmaticdb/query/_query.py +70 -0
- flowmaticdb/query/_select.py +68 -0
- flowmaticdb/query/_simple_mixins.py +142 -0
- flowmaticdb/query/_union.py +15 -0
- flowmaticdb/query/_update.py +26 -0
- flowmaticdb/query/_where_mixin.py +257 -0
- flowmaticdb/query/enums/__init__.py +17 -0
- flowmaticdb/query/enums/_chain.py +6 -0
- flowmaticdb/query/enums/_condition.py +23 -0
- flowmaticdb/query/enums/_join.py +10 -0
- flowmaticdb/query/enums/_order_by_dir.py +6 -0
- flowmaticdb/query/enums/_referential_action.py +10 -0
- flowmaticdb/query/enums/_type.py +9 -0
- flowmaticdb/query/enums/_union.py +6 -0
- flowmaticdb/query/expressions/__init__.py +20 -0
- flowmaticdb/query/expressions/_alias.py +33 -0
- flowmaticdb/query/expressions/_current_timestamp.py +19 -0
- flowmaticdb/query/expressions/_excluded.py +8 -0
- flowmaticdb/query/expressions/_expression.py +25 -0
- flowmaticdb/query/expressions/_identifier.py +22 -0
- flowmaticdb/query/expressions/_raw.py +22 -0
- flowmaticdb/query/expressions/_sql.py +19 -0
- flowmaticdb/query/expressions/_sub_query.py +27 -0
- flowmaticdb/result/__init__.py +14 -0
- flowmaticdb/result/_base.py +41 -0
- flowmaticdb/result/_mysql.py +71 -0
- flowmaticdb/result/_postgres.py +152 -0
- flowmaticdb/result/_result.py +29 -0
- flowmaticdb/result/_sqlite.py +51 -0
- flowmaticdb-1.0.0.dist-info/METADATA +1024 -0
- flowmaticdb-1.0.0.dist-info/RECORD +73 -0
- flowmaticdb-1.0.0.dist-info/WHEEL +5 -0
- flowmaticdb-1.0.0.dist-info/top_level.txt +1 -0
flowmaticdb/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from flowmaticdb._helpers import (
|
|
2
|
+
alias,
|
|
3
|
+
current_timestamp,
|
|
4
|
+
expression,
|
|
5
|
+
identifier,
|
|
6
|
+
now,
|
|
7
|
+
raw,
|
|
8
|
+
sub_query,
|
|
9
|
+
)
|
|
10
|
+
from flowmaticdb._query_with_params import QueryWithParams
|
|
11
|
+
from flowmaticdb.exceptions import AdapterError, DatabaseError, DriverError, QueryError, QueryWithParamsError
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AdapterError",
|
|
15
|
+
"DatabaseError",
|
|
16
|
+
"DriverError",
|
|
17
|
+
"QueryError",
|
|
18
|
+
"QueryWithParams",
|
|
19
|
+
"QueryWithParamsError",
|
|
20
|
+
"alias",
|
|
21
|
+
"current_timestamp",
|
|
22
|
+
"expression",
|
|
23
|
+
"identifier",
|
|
24
|
+
"now",
|
|
25
|
+
"raw",
|
|
26
|
+
"sub_query",
|
|
27
|
+
]
|
flowmaticdb/_helpers.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flowmaticdb.query import SelectQuery
|
|
7
|
+
from flowmaticdb.query.expressions._alias import Alias
|
|
8
|
+
from flowmaticdb.query.expressions._current_timestamp import CurrentTimestamp
|
|
9
|
+
from flowmaticdb.query.expressions._expression import Expression
|
|
10
|
+
from flowmaticdb.query.expressions._identifier import Identifier
|
|
11
|
+
from flowmaticdb.query.expressions._raw import Raw
|
|
12
|
+
from flowmaticdb.query.expressions._sub_query import SubQuery
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def escape_ansi(string: str, chars: str) -> str:
|
|
16
|
+
return string.translate(str.maketrans(chars, chars * 2))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def escape_backslash(string: str, chars: str) -> str:
|
|
20
|
+
return string.translate(str.maketrans(chars, "\\" + chars))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def raw(sql: str) -> Raw:
|
|
24
|
+
return Raw(sql)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def identifier(identifier: str | list[str]) -> Identifier:
|
|
28
|
+
return Identifier(identifier)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def alias(identifier: str | list[str] | Any, alias: str) -> Alias:
|
|
32
|
+
return Alias(identifier, alias)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def expression(sql: str, params: list[Any] | None = None) -> Expression:
|
|
36
|
+
return Expression(sql, params)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sub_query(query: SelectQuery, alias: str) -> SubQuery:
|
|
40
|
+
return SubQuery(query, alias)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def current_timestamp() -> CurrentTimestamp:
|
|
44
|
+
return CurrentTimestamp()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def now() -> datetime:
|
|
48
|
+
return datetime.now(UTC)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flowmaticdb.dialects._base import DialectABC
|
|
9
|
+
|
|
10
|
+
REGEX_PATTERN = re.compile(
|
|
11
|
+
r"""(?x)
|
|
12
|
+
(?:
|
|
13
|
+
'(?:\\.|[^\\'])*'
|
|
14
|
+
|"(?:\\.|[^\\"])*"
|
|
15
|
+
|`(?:\\.|[^\\`])*`
|
|
16
|
+
|\[(?:\\.|[^\[\]])*?\]
|
|
17
|
+
|--[^\r\n]*
|
|
18
|
+
|/\*[\s\S]*?\*/
|
|
19
|
+
|\#.*
|
|
20
|
+
)
|
|
21
|
+
|
|
|
22
|
+
(\?)
|
|
23
|
+
|
|
|
24
|
+
((?<!%)%s)
|
|
25
|
+
""",
|
|
26
|
+
re.MULTILINE,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class QueryWithParams:
|
|
32
|
+
query: str
|
|
33
|
+
params: list[Any] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
def percent_s_to_question_marks(self) -> QueryWithParams:
|
|
36
|
+
def _replacer(match: re.Match[str]) -> str:
|
|
37
|
+
if match.group(2) is not None:
|
|
38
|
+
return "?"
|
|
39
|
+
return match.group(0)
|
|
40
|
+
|
|
41
|
+
query = REGEX_PATTERN.sub(_replacer, self.query)
|
|
42
|
+
return QueryWithParams(query=query, params=list(self.params))
|
|
43
|
+
|
|
44
|
+
def question_marks_to_percent_s(self) -> QueryWithParams:
|
|
45
|
+
def _replacer(match: re.Match[str]) -> str:
|
|
46
|
+
if match.group(1) is not None:
|
|
47
|
+
return "%s"
|
|
48
|
+
return match.group(0)
|
|
49
|
+
|
|
50
|
+
query = REGEX_PATTERN.sub(_replacer, self.query)
|
|
51
|
+
return QueryWithParams(query=query, params=list(self.params))
|
|
52
|
+
|
|
53
|
+
def to_sql(self, dialect: DialectABC) -> str:
|
|
54
|
+
param_idx = 0
|
|
55
|
+
|
|
56
|
+
def _replacer(match: re.Match[str]) -> str:
|
|
57
|
+
nonlocal param_idx
|
|
58
|
+
if match.group(1) is not None or match.group(2) is not None:
|
|
59
|
+
if param_idx < len(self.params):
|
|
60
|
+
value = self.params[param_idx]
|
|
61
|
+
casted = dialect.cast_to_query(value)
|
|
62
|
+
param_idx += 1
|
|
63
|
+
return casted
|
|
64
|
+
return "?"
|
|
65
|
+
return match.group(0)
|
|
66
|
+
|
|
67
|
+
return REGEX_PATTERN.sub(_replacer, self.query)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from flowmaticdb.adapters._base import AdapterABC
|
|
2
|
+
from flowmaticdb.adapters._mysql import MySQLAdapter
|
|
3
|
+
from flowmaticdb.adapters._postgres import PsycopgAdapter
|
|
4
|
+
from flowmaticdb.adapters._sqlite import SQLiteAdapter
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AdapterABC",
|
|
8
|
+
"MySQLAdapter",
|
|
9
|
+
"PsycopgAdapter",
|
|
10
|
+
"SQLiteAdapter",
|
|
11
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from flowmaticdb.result._base import ResultABC
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flowmaticdb._query_with_params import QueryWithParams
|
|
11
|
+
from flowmaticdb.dialects._base import DialectABC
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AdapterABC(ABC):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
driver_name: str,
|
|
18
|
+
database_name: str,
|
|
19
|
+
startup_queries: list[str] | None = None,
|
|
20
|
+
options: dict[str, Any] | None = None,
|
|
21
|
+
debug_callback: Callable[[str, float, str | None], None] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._driver_name = driver_name
|
|
24
|
+
self._database_name = database_name
|
|
25
|
+
self._startup_queries = startup_queries or []
|
|
26
|
+
self._options = options or {}
|
|
27
|
+
self._debug_callback = debug_callback
|
|
28
|
+
self._in_transaction = False
|
|
29
|
+
|
|
30
|
+
def _exec_startup_queries(self) -> None:
|
|
31
|
+
for query in self._startup_queries:
|
|
32
|
+
self.exec(query)
|
|
33
|
+
|
|
34
|
+
def _debug(self, sql: str, duration: float, error: str | None = None) -> None:
|
|
35
|
+
if self._debug_callback is not None:
|
|
36
|
+
self._debug_callback(sql, duration, error)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def driver_name(self) -> str:
|
|
40
|
+
return self._driver_name
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def database_name(self) -> str:
|
|
44
|
+
return self._database_name
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def version(self) -> str:
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def exec(self, query: str) -> None:
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def query(self, query: str) -> ResultABC:
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def query_with_params(
|
|
60
|
+
self,
|
|
61
|
+
dialect: DialectABC,
|
|
62
|
+
query_with_params: QueryWithParams,
|
|
63
|
+
emulate_prepare: bool = False,
|
|
64
|
+
) -> ResultABC:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def begin_transaction(self) -> None:
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def commit_transaction(self) -> None:
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def rollback_transaction(self) -> None:
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def begin_savepoint(self, name: str) -> None:
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def commit_savepoint(self, name: str) -> None:
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def rollback_savepoint(self, name: str) -> None:
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def in_transaction(self) -> bool:
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def last_insert_id(self, name: str | None = None) -> int | str | None:
|
|
98
|
+
...
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from flowmaticdb.adapters._base import AdapterABC
|
|
8
|
+
from flowmaticdb.result._base import ResultABC
|
|
9
|
+
from flowmaticdb.result._mysql import MySQLResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from flowmaticdb._query_with_params import QueryWithParams
|
|
13
|
+
from flowmaticdb.dialects._base import DialectABC
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MySQLAdapter(AdapterABC):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
database_name: str,
|
|
20
|
+
startup_queries: list[str] | None = None,
|
|
21
|
+
options: dict[str, Any] | None = None,
|
|
22
|
+
debug_callback: Callable[[str, float, str | None], None] | None = None,
|
|
23
|
+
host: str = "localhost",
|
|
24
|
+
port: int = 3306,
|
|
25
|
+
user: str = "root",
|
|
26
|
+
password: str = "",
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(
|
|
29
|
+
driver_name="mysql",
|
|
30
|
+
database_name=database_name,
|
|
31
|
+
startup_queries=startup_queries,
|
|
32
|
+
options=options,
|
|
33
|
+
debug_callback=debug_callback,
|
|
34
|
+
)
|
|
35
|
+
self._host = host
|
|
36
|
+
self._port = port
|
|
37
|
+
self._user = user
|
|
38
|
+
self._password = password
|
|
39
|
+
self._connection: Any = None
|
|
40
|
+
self._current_cursor: Any = None
|
|
41
|
+
self._connect()
|
|
42
|
+
|
|
43
|
+
def _connect(self) -> None:
|
|
44
|
+
import mysql.connector
|
|
45
|
+
|
|
46
|
+
kwargs: dict[str, Any] = {
|
|
47
|
+
"host": self._host,
|
|
48
|
+
"port": self._port,
|
|
49
|
+
"database": self._database_name,
|
|
50
|
+
"user": self._user,
|
|
51
|
+
"password": self._password,
|
|
52
|
+
"autocommit": True,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ssl_mode = self._options.get("ssl_mode")
|
|
56
|
+
if ssl_mode:
|
|
57
|
+
kwargs["ssl_mode"] = ssl_mode
|
|
58
|
+
|
|
59
|
+
connect_timeout = self._options.get("connect_timeout")
|
|
60
|
+
if connect_timeout:
|
|
61
|
+
kwargs["connect_timeout"] = connect_timeout
|
|
62
|
+
|
|
63
|
+
charset = self._options.get("charset", "utf8mb4")
|
|
64
|
+
kwargs["charset"] = charset
|
|
65
|
+
|
|
66
|
+
self._connection = mysql.connector.connect(**kwargs)
|
|
67
|
+
|
|
68
|
+
self._exec_startup_queries()
|
|
69
|
+
|
|
70
|
+
def _drain_cursor(self) -> None:
|
|
71
|
+
"""Drain any unread results from the previous cursor.
|
|
72
|
+
|
|
73
|
+
MySQL connector forbids creating a new cursor while the previous
|
|
74
|
+
one still has unread rows. This method consumes and discards any
|
|
75
|
+
remaining rows so the next query can proceed.
|
|
76
|
+
"""
|
|
77
|
+
if self._current_cursor is not None:
|
|
78
|
+
try:
|
|
79
|
+
self._current_cursor.fetchall()
|
|
80
|
+
except Exception: # noqa: BLE001, S110
|
|
81
|
+
pass
|
|
82
|
+
self._current_cursor = None
|
|
83
|
+
|
|
84
|
+
def version(self) -> str:
|
|
85
|
+
try:
|
|
86
|
+
cursor = self._connection.execute("SELECT VERSION()")
|
|
87
|
+
row = cursor.fetchone()
|
|
88
|
+
if row:
|
|
89
|
+
return str(row[0])
|
|
90
|
+
return "0"
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
92
|
+
return "0"
|
|
93
|
+
|
|
94
|
+
def exec(self, query: str) -> None:
|
|
95
|
+
start = time.time()
|
|
96
|
+
error: str | None = None
|
|
97
|
+
try:
|
|
98
|
+
self._drain_cursor()
|
|
99
|
+
cursor = self._connection.cursor()
|
|
100
|
+
cursor.execute(query)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
error = str(e)
|
|
103
|
+
raise
|
|
104
|
+
finally:
|
|
105
|
+
duration = time.time() - start
|
|
106
|
+
self._debug(query, duration, error)
|
|
107
|
+
|
|
108
|
+
def query(self, query: str) -> ResultABC:
|
|
109
|
+
start = time.time()
|
|
110
|
+
error: str | None = None
|
|
111
|
+
try:
|
|
112
|
+
self._drain_cursor()
|
|
113
|
+
cursor = self._connection.cursor()
|
|
114
|
+
cursor.execute(query)
|
|
115
|
+
self._current_cursor = cursor
|
|
116
|
+
return MySQLResult(cursor)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error = str(e)
|
|
119
|
+
raise
|
|
120
|
+
finally:
|
|
121
|
+
duration = time.time() - start
|
|
122
|
+
self._debug(query, duration, error)
|
|
123
|
+
|
|
124
|
+
def query_with_params(
|
|
125
|
+
self,
|
|
126
|
+
dialect: DialectABC,
|
|
127
|
+
query_with_params: QueryWithParams,
|
|
128
|
+
emulate_prepare: bool = False,
|
|
129
|
+
) -> ResultABC:
|
|
130
|
+
query_with_params = query_with_params.question_marks_to_percent_s()
|
|
131
|
+
sql = query_with_params.query
|
|
132
|
+
params = query_with_params.params
|
|
133
|
+
|
|
134
|
+
start = time.time()
|
|
135
|
+
error: str | None = None
|
|
136
|
+
try:
|
|
137
|
+
self._drain_cursor()
|
|
138
|
+
if emulate_prepare:
|
|
139
|
+
sql_full = query_with_params.to_sql(dialect)
|
|
140
|
+
cursor = self._connection.cursor()
|
|
141
|
+
cursor.execute(sql_full)
|
|
142
|
+
else:
|
|
143
|
+
cursor = self._connection.cursor()
|
|
144
|
+
cursor.execute(sql, params)
|
|
145
|
+
self._current_cursor = cursor
|
|
146
|
+
return MySQLResult(cursor)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
error = str(e)
|
|
149
|
+
raise
|
|
150
|
+
finally:
|
|
151
|
+
duration = time.time() - start
|
|
152
|
+
self._debug(query_with_params.to_sql(dialect), duration, error)
|
|
153
|
+
|
|
154
|
+
def begin_transaction(self) -> None:
|
|
155
|
+
self._connection.start_transaction()
|
|
156
|
+
self._in_transaction = True
|
|
157
|
+
|
|
158
|
+
def commit_transaction(self) -> None:
|
|
159
|
+
self._connection.commit()
|
|
160
|
+
self._in_transaction = False
|
|
161
|
+
|
|
162
|
+
def rollback_transaction(self) -> None:
|
|
163
|
+
self._connection.rollback()
|
|
164
|
+
self._in_transaction = False
|
|
165
|
+
|
|
166
|
+
def begin_savepoint(self, name: str) -> None:
|
|
167
|
+
self._drain_cursor()
|
|
168
|
+
cursor = self._connection.cursor()
|
|
169
|
+
cursor.execute(f"SAVEPOINT {name}")
|
|
170
|
+
|
|
171
|
+
def commit_savepoint(self, name: str) -> None:
|
|
172
|
+
self._drain_cursor()
|
|
173
|
+
cursor = self._connection.cursor()
|
|
174
|
+
cursor.execute(f"RELEASE SAVEPOINT {name}")
|
|
175
|
+
|
|
176
|
+
def rollback_savepoint(self, name: str) -> None:
|
|
177
|
+
self._drain_cursor()
|
|
178
|
+
cursor = self._connection.cursor()
|
|
179
|
+
cursor.execute(f"ROLLBACK TO SAVEPOINT {name}")
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def in_transaction(self) -> bool:
|
|
183
|
+
return self._in_transaction
|
|
184
|
+
|
|
185
|
+
def last_insert_id(self, name: str | None = None) -> int | str | None:
|
|
186
|
+
self._drain_cursor()
|
|
187
|
+
cursor = self._connection.cursor()
|
|
188
|
+
cursor.execute("SELECT LAST_INSERT_ID()")
|
|
189
|
+
row = cursor.fetchone()
|
|
190
|
+
return row[0] if row else None
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from flowmaticdb.adapters._base import AdapterABC
|
|
8
|
+
from flowmaticdb.result._base import ResultABC
|
|
9
|
+
from flowmaticdb.result._postgres import PsycopgResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from flowmaticdb._query_with_params import QueryWithParams
|
|
13
|
+
from flowmaticdb.dialects._base import DialectABC
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PsycopgAdapter(AdapterABC):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
database_name: str,
|
|
20
|
+
startup_queries: list[str] | None = None,
|
|
21
|
+
options: dict[str, Any] | None = None,
|
|
22
|
+
debug_callback: Callable[[str, float, str | None], None] | None = None,
|
|
23
|
+
host: str = "localhost",
|
|
24
|
+
port: int = 5432,
|
|
25
|
+
user: str = "postgres",
|
|
26
|
+
password: str = "",
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(
|
|
29
|
+
driver_name="postgresql",
|
|
30
|
+
database_name=database_name,
|
|
31
|
+
startup_queries=startup_queries,
|
|
32
|
+
options=options,
|
|
33
|
+
debug_callback=debug_callback,
|
|
34
|
+
)
|
|
35
|
+
self._host = host
|
|
36
|
+
self._port = port
|
|
37
|
+
self._user = user
|
|
38
|
+
self._password = password
|
|
39
|
+
self._connection: Any = None
|
|
40
|
+
self._connect()
|
|
41
|
+
|
|
42
|
+
def _connect(self) -> None:
|
|
43
|
+
import psycopg
|
|
44
|
+
|
|
45
|
+
kwargs: dict[str, Any] = {
|
|
46
|
+
"host": self._host,
|
|
47
|
+
"port": self._port,
|
|
48
|
+
"dbname": self._database_name,
|
|
49
|
+
"user": self._user,
|
|
50
|
+
"password": self._password,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ssl_mode = self._options.get("sslmode")
|
|
54
|
+
if ssl_mode:
|
|
55
|
+
kwargs["sslmode"] = ssl_mode
|
|
56
|
+
|
|
57
|
+
search_path = self._options.get("search_path")
|
|
58
|
+
if search_path:
|
|
59
|
+
kwargs["options"] = f"-c search_path={search_path}"
|
|
60
|
+
|
|
61
|
+
self._connection = psycopg.connect(**kwargs)
|
|
62
|
+
|
|
63
|
+
self._exec_startup_queries()
|
|
64
|
+
|
|
65
|
+
def version(self) -> str:
|
|
66
|
+
try:
|
|
67
|
+
cursor = self._connection.execute("SELECT version()")
|
|
68
|
+
row = cursor.fetchone()
|
|
69
|
+
if row:
|
|
70
|
+
version_str = str(row[0])
|
|
71
|
+
import re
|
|
72
|
+
match = re.search(r'(\d+\.\d+(?:\.\d+)?)', version_str)
|
|
73
|
+
if match:
|
|
74
|
+
return match.group(1)
|
|
75
|
+
return "0"
|
|
76
|
+
except Exception: # noqa: BLE001
|
|
77
|
+
return "0"
|
|
78
|
+
|
|
79
|
+
def exec(self, query: str) -> None:
|
|
80
|
+
start = time.time()
|
|
81
|
+
error: str | None = None
|
|
82
|
+
try:
|
|
83
|
+
self._connection.execute(query)
|
|
84
|
+
self._connection.commit()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
error = str(e)
|
|
87
|
+
raise
|
|
88
|
+
finally:
|
|
89
|
+
duration = time.time() - start
|
|
90
|
+
self._debug(query, duration, error)
|
|
91
|
+
|
|
92
|
+
def query(self, query: str) -> ResultABC:
|
|
93
|
+
start = time.time()
|
|
94
|
+
error: str | None = None
|
|
95
|
+
try:
|
|
96
|
+
cursor = self._connection.execute(query)
|
|
97
|
+
return PsycopgResult(cursor)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
error = str(e)
|
|
100
|
+
raise
|
|
101
|
+
finally:
|
|
102
|
+
duration = time.time() - start
|
|
103
|
+
self._debug(query, duration, error)
|
|
104
|
+
|
|
105
|
+
def query_with_params(
|
|
106
|
+
self,
|
|
107
|
+
dialect: DialectABC,
|
|
108
|
+
query_with_params: QueryWithParams,
|
|
109
|
+
emulate_prepare: bool = False,
|
|
110
|
+
) -> ResultABC:
|
|
111
|
+
query_with_params = query_with_params.question_marks_to_percent_s()
|
|
112
|
+
sql = query_with_params.query
|
|
113
|
+
params = query_with_params.params
|
|
114
|
+
|
|
115
|
+
start = time.time()
|
|
116
|
+
error: str | None = None
|
|
117
|
+
try:
|
|
118
|
+
if emulate_prepare:
|
|
119
|
+
sql_full = query_with_params.to_sql(dialect)
|
|
120
|
+
cursor = self._connection.execute(sql_full)
|
|
121
|
+
else:
|
|
122
|
+
cursor = self._connection.execute(sql, params)
|
|
123
|
+
return PsycopgResult(cursor)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
error = str(e)
|
|
126
|
+
raise
|
|
127
|
+
finally:
|
|
128
|
+
duration = time.time() - start
|
|
129
|
+
self._debug(query_with_params.to_sql(dialect), duration, error)
|
|
130
|
+
|
|
131
|
+
def begin_transaction(self) -> None:
|
|
132
|
+
self._connection.execute("BEGIN TRANSACTION")
|
|
133
|
+
self._in_transaction = True
|
|
134
|
+
|
|
135
|
+
def commit_transaction(self) -> None:
|
|
136
|
+
self._connection.commit()
|
|
137
|
+
self._in_transaction = False
|
|
138
|
+
|
|
139
|
+
def rollback_transaction(self) -> None:
|
|
140
|
+
self._connection.rollback()
|
|
141
|
+
self._in_transaction = False
|
|
142
|
+
|
|
143
|
+
def begin_savepoint(self, name: str) -> None:
|
|
144
|
+
self._connection.execute(f"SAVEPOINT {name}")
|
|
145
|
+
|
|
146
|
+
def commit_savepoint(self, name: str) -> None:
|
|
147
|
+
self._connection.execute(f"RELEASE SAVEPOINT {name}")
|
|
148
|
+
|
|
149
|
+
def rollback_savepoint(self, name: str) -> None:
|
|
150
|
+
self._connection.execute(f"ROLLBACK TO SAVEPOINT {name}")
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def in_transaction(self) -> bool:
|
|
154
|
+
return self._connection.info.transaction_status is not None
|
|
155
|
+
|
|
156
|
+
def last_insert_id(self, name: str | None = None) -> int | str | None:
|
|
157
|
+
if name:
|
|
158
|
+
cursor = self._connection.execute(f"SELECT currval('{name}')")
|
|
159
|
+
else:
|
|
160
|
+
cursor = self._connection.execute("SELECT lastval()")
|
|
161
|
+
row = cursor.fetchone()
|
|
162
|
+
return row[0] if row else None
|