Skip to content

Commit

Permalink
snowflake support and general expansion of user managed DB support
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisclark committed Jul 8, 2024
1 parent 372c7e0 commit a0ff36a
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 2 deletions.
27 changes: 27 additions & 0 deletions explorer/ee/db_connections/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
from django import forms
from explorer.ee.db_connections.models import DatabaseConnection
import json
from django.core.exceptions import ValidationError


class JSONTextInput(forms.TextInput):
def render(self, name, value, attrs=None, renderer=None):
if value in (None, '', 'null'):

Check failure on line 9 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:9:28: Q000 Single quotes found but double quotes preferred

Check failure on line 9 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:9:32: Q000 Single quotes found but double quotes preferred

Check failure on line 9 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:9:28: Q000 Single quotes found but double quotes preferred

Check failure on line 9 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:9:32: Q000 Single quotes found but double quotes preferred
value = ''

Check failure on line 10 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:10:21: Q000 Single quotes found but double quotes preferred

Check failure on line 10 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:10:21: Q000 Single quotes found but double quotes preferred
elif isinstance(value, dict):
value = json.dumps(value)
return super().render(name, value, attrs, renderer)

def value_from_datadict(self, data, files, name):
value = data.get(name)
if value in (None, '', 'null'):

Check failure on line 17 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:17:28: Q000 Single quotes found but double quotes preferred

Check failure on line 17 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:17:32: Q000 Single quotes found but double quotes preferred

Check failure on line 17 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:17:28: Q000 Single quotes found but double quotes preferred

Check failure on line 17 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/ee/db_connections/forms.py:17:32: Q000 Single quotes found but double quotes preferred
return None
try:
return json.loads(value)
except (TypeError, ValueError):
raise ValidationError("Enter a valid JSON")

Check failure on line 22 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (B904)

explorer/ee/db_connections/forms.py:22:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

Check failure on line 22 in explorer/ee/db_connections/forms.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (B904)

explorer/ee/db_connections/forms.py:22:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling


class DatabaseConnectionForm(forms.ModelForm):
Expand All @@ -14,4 +34,11 @@ class Meta:
"password": forms.PasswordInput(attrs={"class": "form-control"}),
"host": forms.TextInput(attrs={"class": "form-control"}),
"port": forms.TextInput(attrs={"class": "form-control"}),
"extras": JSONTextInput(attrs={"class": "form-control"}),
}

help_texts = {
"extras": "You can provide JSON that will get merged into the final connection object. \
The result should be a valid Django database connection dictionary."

}
5 changes: 4 additions & 1 deletion explorer/ee/db_connections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class DatabaseConnection(models.Model):
("django.db.backends.oracle", "Oracle"),
("django.db.backends.mysql", "MariaDB"),
("django_cockroachdb", "CockroachDB"),
("django.db.backends.sqlserver", "SQL Server (mssql-django)"),
("mssql", "SQL Server (mssql-django)"),
("django_snowflake", "Snowflake"),
)

alias = models.CharField(max_length=255, unique=True)
Expand All @@ -31,6 +32,7 @@ class DatabaseConnection(models.Model):
password = encrypt(models.CharField(max_length=255, blank=True))
host = encrypt(models.CharField(max_length=255, blank=True))
port = models.CharField(max_length=255, blank=True)
extras = models.JSONField(blank=True, null=True)

def __str__(self):
return f"{self.name} ({self.alias})"
Expand Down Expand Up @@ -58,6 +60,7 @@ def from_django_connection(cls, connection_alias):
port=conn.get("PORT"),
)


@receiver(pre_save, sender=DatabaseConnection)
def validate_database_connection(sender, instance, **kwargs):
if instance.name in settings.DATABASES.keys():
Expand Down
6 changes: 6 additions & 0 deletions explorer/ee/db_connections/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.db import DatabaseError
from django.db.utils import load_backend
import os
import json

from dateutil import parser

Expand Down Expand Up @@ -76,6 +77,11 @@ def create_django_style_connection(explorer_connection):
"ATOMIC_REQUESTS": False,
}

if explorer_connection.extras:
extras_dict = json.loads(explorer_connection.extras) if isinstance(explorer_connection.extras,
str) else explorer_connection.extras
connection_settings.update(extras_dict)

try:
backend = load_backend(explorer_connection.engine)
return backend.DatabaseWrapper(connection_settings, explorer_connection.alias)
Expand Down
23 changes: 23 additions & 0 deletions explorer/migrations/0020_databaseconnection_extras_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-07-08 01:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('explorer', '0019_alter_databaseconnection_engine'),
]

operations = [
migrations.AddField(
model_name='databaseconnection',
name='extras',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='databaseconnection',
name='engine',
field=models.CharField(choices=[('django.db.backends.sqlite3', 'SQLite3'), ('django.db.backends.postgresql', 'PostgreSQL'), ('django.db.backends.mysql', 'MySQL'), ('django.db.backends.oracle', 'Oracle'), ('django.db.backends.mysql', 'MariaDB'), ('django_cockroachdb', 'CockroachDB'), ('mssql', 'SQL Server (mssql-django)'), ('django_snowflake', 'Snowflake')], max_length=255),
),
]
2 changes: 1 addition & 1 deletion explorer/templates/connections/connections.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ <h3>Connections</h3>
{% endif %}
</td>
<td>{{ connection.name }}</td>
<td>{{ connection.get_engine_display }}</td>
<td>{{ connection.get_engine_display }}{% if connection.is_upload %} (uploaded){% endif %}</td>
<td>
<a href="../play/?connection={{ connection.alias }}" class="px-2"><i class="bi-arrow-up-right-square small me-1"></i>Query</a>
{% if connection.id %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ <h2>Connection Details</h2>
<th>Port</th>
<td>{{ object.port }}</td>
</tr>
<tr>
<th>Extras</th>
<td>{{ object.extras }}</td>
</tr>
{% endif %}
</table>
{% if object.is_upload %}
Expand Down
18 changes: 18 additions & 0 deletions explorer/tests/test_db_connection_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ def test_create_django_style_connection_non_sqlite(self, mock_load_backend):
mock_load_backend.assert_called_once_with("django.db.backends.postgresql")
mock_backend.DatabaseWrapper.assert_called_once()

@patch("explorer.ee.db_connections.utils.load_backend")
def test_create_django_style_connection_with_extras(self, mock_load_backend):
mock_explorer_connection = MagicMock()
mock_explorer_connection.is_upload = False
mock_explorer_connection.engine = "django.db.backends.postgresql"
mock_explorer_connection.extras = '{"sslmode": "require", "connect_timeout": 10}'

mock_backend = MagicMock()
mock_load_backend.return_value = mock_backend

create_django_style_connection(mock_explorer_connection)

mock_load_backend.assert_called_once_with("django.db.backends.postgresql")
mock_backend.DatabaseWrapper.assert_called_once()
args, kwargs = mock_backend.DatabaseWrapper.call_args
self.assertEqual(args[0]['sslmode'], "require")

Check failure on line 161 in explorer/tests/test_db_connection_utils.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/tests/test_db_connection_utils.py:161:34: Q000 Single quotes found but double quotes preferred

Check failure on line 161 in explorer/tests/test_db_connection_utils.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/tests/test_db_connection_utils.py:161:34: Q000 Single quotes found but double quotes preferred
self.assertEqual(args[0]['connect_timeout'], 10)

Check failure on line 162 in explorer/tests/test_db_connection_utils.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/tests/test_db_connection_utils.py:162:34: Q000 Single quotes found but double quotes preferred

Check failure on line 162 in explorer/tests/test_db_connection_utils.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (Q000)

explorer/tests/test_db_connection_utils.py:162:34: Q000 Single quotes found but double quotes preferred


@skipIf(not EXPLORER_USER_UPLOADS_ENABLED, "User uploads not enabled")
class TestPandasToSQLite(TestCase):
Expand Down
1 change: 1 addition & 0 deletions explorer/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def test_create_db_connection_from_django_connection(self):
c = DatabaseConnection.from_django_connection(app_settings.EXPLORER_DEFAULT_CONNECTION)
self.assertEqual(c.name, "tst1")
self.assertEqual(c.alias, "default")
self.assertIsNone(c.extras)

@patch("os.makedirs")
@patch("os.path.exists", return_value=False)
Expand Down

0 comments on commit a0ff36a

Please sign in to comment.