Skip to content

Commit 26cfe23

Browse files
feat: add Django support for MySQL (#1077)
1 parent f8f5acf commit 26cfe23

16 files changed

Lines changed: 3166 additions & 4 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License").
4+
# You may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License").
4+
# You may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License").
4+
# You may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License").
4+
# You may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Any
16+
17+
import mysql.connector
18+
import mysql.connector.django.base as base
19+
from django.utils.asyncio import async_unsafe
20+
from django.utils.functional import cached_property
21+
from django.utils.regex_helper import _lazy_re_compile
22+
23+
from aws_advanced_python_wrapper import AwsWrapperConnection
24+
25+
# This should match the numerical portion of the version numbers (we can treat
26+
# versions like 5.0.24 and 5.0.24a as the same).
27+
server_version_re = _lazy_re_compile(r"(\d{1,2})\.(\d{1,2})\.(\d{1,2})")
28+
29+
30+
class DatabaseWrapper(base.DatabaseWrapper):
31+
"""Custom MySQL Connector backend for Django"""
32+
33+
def __init__(self, *args: Any, **kwargs: Any) -> None:
34+
super().__init__(*args, **kwargs)
35+
self._read_only = False
36+
37+
@async_unsafe
38+
def get_new_connection(self, conn_params):
39+
if "converter_class" not in conn_params:
40+
conn_params["converter_class"] = base.DjangoMySQLConverter
41+
conn = AwsWrapperConnection.connect(
42+
mysql.connector.Connect,
43+
**conn_params
44+
)
45+
46+
if not self._read_only:
47+
return conn
48+
else:
49+
conn.read_only = True
50+
return conn
51+
52+
def get_connection_params(self):
53+
kwargs = super().get_connection_params()
54+
self._read_only = kwargs.pop("read_only", False)
55+
return kwargs
56+
57+
@cached_property
58+
def mysql_server_info(self):
59+
return self.mysql_server_data["version"]
60+
61+
@cached_property
62+
def mysql_version(self):
63+
match = server_version_re.match(self.mysql_server_info)
64+
if not match:
65+
raise Exception(
66+
"Unable to determine MySQL version from version string %r"
67+
% self.mysql_server_info
68+
)
69+
return tuple(int(x) for x in match.groups())
70+
71+
@cached_property
72+
def mysql_is_mariadb(self):
73+
return "mariadb" in self.mysql_server_info.lower()
74+
75+
@cached_property
76+
def sql_mode(self):
77+
sql_mode = self.mysql_server_data["sql_mode"]
78+
return set(sql_mode.split(",") if sql_mode else ())

aws_advanced_python_wrapper/wrapper.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ def rowcount(self) -> int:
267267
def arraysize(self) -> int:
268268
return self.target_cursor.arraysize
269269

270+
# Optional for PEP249
271+
@property
272+
def lastrowid(self) -> int:
273+
return self.target_cursor.lastrowid # type: ignore[attr-defined]
274+
270275
def close(self) -> None:
271276
self._plugin_manager.execute(self.target_cursor, DbApiMethod.CURSOR_CLOSE,
272277
lambda: self.target_cursor.close())
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License").
4+
# You may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Django ORM Failover Example with AWS Advanced Python Wrapper
17+
18+
This example demonstrates how to handle failover events when using Django ORM
19+
with the AWS Advanced Python Wrapper.
20+
21+
"""
22+
23+
import django
24+
from django.conf import settings
25+
from django.db import connection, models
26+
27+
from aws_advanced_python_wrapper import release_resources
28+
from aws_advanced_python_wrapper.errors import (
29+
FailoverFailedError, FailoverSuccessError,
30+
TransactionResolutionUnknownError)
31+
32+
# Django settings configuration
33+
DJANGO_SETTINGS = {
34+
'DATABASES': {
35+
'default': {
36+
'ENGINE': 'aws_advanced_python_wrapper.django.backends.mysql_connector',
37+
'NAME': 'test_db',
38+
'USER': 'admin',
39+
'PASSWORD': 'password',
40+
'HOST': 'database.cluster-xyz.us-east-1.rds.amazonaws.com',
41+
'PORT': 3306,
42+
'OPTIONS': {
43+
'plugins': 'failover_v2',
44+
'connect_timeout': 10,
45+
'autocommit': True,
46+
},
47+
},
48+
},
49+
}
50+
51+
# Configure Django settings
52+
if not settings.configured:
53+
settings.configure(**DJANGO_SETTINGS)
54+
django.setup()
55+
56+
57+
class BankAccount(models.Model):
58+
"""Example model for demonstrating failover handling."""
59+
name: str = models.CharField(max_length=100) # type: ignore[assignment]
60+
account_balance: int = models.IntegerField() # type: ignore[assignment]
61+
62+
class Meta:
63+
app_label = 'myapp'
64+
db_table = 'bank_test'
65+
66+
def __str__(self) -> str:
67+
return f"{self.name}: ${self.account_balance}"
68+
69+
70+
def execute_query_with_failover_handling(query_func):
71+
"""
72+
Execute a Django ORM query with failover error handling.
73+
74+
Args:
75+
query_func: A callable that executes the desired query
76+
77+
Returns:
78+
The result of the query function
79+
"""
80+
try:
81+
return query_func()
82+
83+
except FailoverSuccessError:
84+
# Query execution failed and AWS Advanced Python Wrapper successfully failed over to an available instance.
85+
# https://github.com/aws/aws-advanced-python-wrapper/blob/main/docs/using-the-python-driver/using-plugins/UsingTheFailoverPlugin.md#failoversuccesserror
86+
87+
# The connection has been re-established. Retry the query.
88+
print("Failover successful! Retrying query...")
89+
90+
# Retry the query
91+
return query_func()
92+
93+
except FailoverFailedError as e:
94+
# Failover failed. The application should open a new connection,
95+
# check the results of the failed transaction and re-run it if needed.
96+
# https://github.com/aws/aws-advanced-python-wrapper/blob/main/docs/using-the-python-driver/using-plugins/UsingTheFailoverPlugin.md#failoverfailederror
97+
print(f"Failover failed: {e}")
98+
print("Application should open a new connection and retry the transaction.")
99+
raise e
100+
101+
except TransactionResolutionUnknownError as e:
102+
# The transaction state is unknown. The application should check the status
103+
# of the failed transaction and restart it if needed.
104+
# https://github.com/aws/aws-advanced-python-wrapper/blob/main/docs/using-the-python-driver/using-plugins/UsingTheFailoverPlugin.md#transactionresolutionunknownerror
105+
print(f"Transaction resolution unknown: {e}")
106+
print("Application should check transaction status and retry if needed.")
107+
raise e
108+
109+
110+
def create_table():
111+
"""Create the database table with failover handling."""
112+
def _create():
113+
with connection.cursor() as cursor:
114+
cursor.execute("""
115+
CREATE TABLE IF NOT EXISTS bank_test (
116+
id INT AUTO_INCREMENT PRIMARY KEY,
117+
name VARCHAR(100),
118+
account_balance INT
119+
)
120+
""")
121+
print("Table created successfully")
122+
123+
execute_query_with_failover_handling(_create)
124+
125+
126+
def drop_table():
127+
"""Drop the database table with failover handling."""
128+
def _drop():
129+
with connection.cursor() as cursor:
130+
cursor.execute("DROP TABLE IF EXISTS bank_test")
131+
print("Table dropped successfully")
132+
133+
execute_query_with_failover_handling(_drop)
134+
135+
136+
def insert_records():
137+
"""Insert records with failover handling."""
138+
print("\n--- Inserting Records ---")
139+
140+
def _insert1():
141+
account = BankAccount.objects.create(name="Jane Doe", account_balance=200)
142+
print(f"Inserted: {account}")
143+
return account
144+
145+
def _insert2():
146+
account = BankAccount.objects.create(name="John Smith", account_balance=200)
147+
print(f"Inserted: {account}")
148+
return account
149+
150+
execute_query_with_failover_handling(_insert1)
151+
execute_query_with_failover_handling(_insert2)
152+
153+
154+
def query_records():
155+
"""Query records with failover handling."""
156+
print("\n--- Querying Records ---")
157+
158+
def _query():
159+
accounts = list(BankAccount.objects.all())
160+
for account in accounts:
161+
print(f" {account}")
162+
return accounts
163+
164+
return execute_query_with_failover_handling(_query)
165+
166+
167+
def update_record():
168+
"""Update a record with failover handling."""
169+
print("\n--- Updating Record ---")
170+
171+
def _update():
172+
account = BankAccount.objects.filter(name="Jane Doe").first()
173+
if account:
174+
account.account_balance = 300
175+
account.save()
176+
print(f"Updated: {account}")
177+
return account
178+
179+
return execute_query_with_failover_handling(_update)
180+
181+
182+
def filter_records():
183+
"""Filter records with failover handling."""
184+
print("\n--- Filtering Records ---")
185+
186+
def _filter():
187+
accounts = list(BankAccount.objects.filter(account_balance__gte=250))
188+
print(f"Found {len(accounts)} accounts with balance >= $250:")
189+
for account in accounts:
190+
print(f" {account}")
191+
return accounts
192+
193+
return execute_query_with_failover_handling(_filter)
194+
195+
196+
if __name__ == "__main__":
197+
try:
198+
print("Django ORM Failover Example with AWS Advanced Python Wrapper")
199+
print("=" * 60)
200+
201+
# Create table
202+
create_table()
203+
204+
# Insert records
205+
insert_records()
206+
207+
# Query records
208+
query_records()
209+
210+
# Update a record
211+
update_record()
212+
213+
# Query again to see the update
214+
query_records()
215+
216+
# Filter records
217+
filter_records()
218+
219+
# Cleanup
220+
print("\n--- Cleanup ---")
221+
drop_table()
222+
223+
print("\n" + "=" * 60)
224+
print("Example completed successfully!")
225+
226+
except Exception as e:
227+
print(f"Error: {e}")
228+
import traceback
229+
traceback.print_exc()
230+
231+
finally:
232+
# Clean up AWS Advanced Python Wrapper resources
233+
release_resources()

0 commit comments

Comments
 (0)