├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── django_app ├── __init__.py ├── app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── perf_test.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_remove_biginteger_integer.py │ │ └── __init__.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── big_integer.py │ │ ├── binary.py │ │ ├── boolean.py │ │ ├── char.py │ │ ├── date.py │ │ ├── datetime.py │ │ ├── decimal.py │ │ ├── float.py │ │ ├── foreign_key.py │ │ ├── generic_ip_address.py │ │ ├── integer.py │ │ ├── many_to_many.py │ │ ├── null_boolean.py │ │ ├── one_to_one.py │ │ ├── positive_integer.py │ │ ├── positive_small_integer.py │ │ ├── slug.py │ │ ├── small_integer.py │ │ ├── text.py │ │ ├── time.py │ │ └── uuid.py │ ├── utils.py │ └── views.py ├── manage.py ├── settings.py ├── urls.py └── wsgi.py ├── django_tarantool ├── __init__.py └── backend │ ├── __init__.py │ ├── base.py │ ├── client.py │ ├── creation.py │ ├── features.py │ ├── introspection.py │ ├── operations.py │ ├── schema.py │ └── utils.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__/ 2 | *.pyc 3 | venv 4 | .idea 5 | tarantool_db 6 | sqlite.db 7 | django_app/sqlite.db 8 | .pytest_cache 9 | tarantool 10 | *.log 11 | .idea 12 | /django_tarantool.egg-info/ 13 | /build/ 14 | /dist/ 15 | /_tarantool/ 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - '3.6' 5 | 6 | install: 7 | - curl -L https://tarantool.io/installer.sh | VER=2.3 sudo -E bash 8 | - pip install -r requirements.txt 9 | 10 | script: 11 | - cd django_app && python3 manage.py test && cd .. 12 | 13 | deploy: 14 | provider: pypi 15 | username: "__token__" 16 | password: 17 | secure: "EWmmZCZcCCtMvlvEQNZkk3aBOPDa2AdJcklMnCHDfzXX3YOhnJQOpTYCTAdDIIDx/pvx9SIo6sDyoHhydbGh2a8D1dyIudW1dwBqKIJLcBU3fJ+/BkjUadw4eqToIiqP1jyZ2LEKillLmgtetkewkyPwXKhZTMK9mkQtWzZ6T6nbN20SlXAF+/7h1+cuFFiKuV+EsriJUIGINHEwUG4l6h2l32wlPKwQ+nOIVldJLjGznOdQ7yBLf1iup2pQ6dyk/CwovW+apAxgLOPpf7CkFUzTxpM5WjL+bUdsVi5eOIKnOwSHYow5PyGNa2X8Q/x31UL/YB9Vyu+UQYLjlsOIj4GYJeOGugn3ei6HrLtCNroPB3QVWbLO3VRhb9u+gxFFcqZT2x9jF0Lun8UcAllPxeHpDjG6rcf2U8uIKsRuWOr3F8AwH6k3Nv3BNKATodOnAQFj0VOZAJrRRg7lx328Ez4tPQyZJjUEJwRBTg9irL7mqRhewHQzhZ5nuw9PFY+s5jkcu/2p8s2VfleDI3b/lbLuNWYaJQcwTfYm+XoyZupPj9ayjiTuI8yTE7yczoI/k4kcxewq8na5f5CKXXph0NrY+4/BJbZlWP2HvsDrF5MeIU90xuDMgH01CUaU7LXmj/8fSL0/WHhMLECRYqGJzvPBFJcyli8k9RyjvdikJEY=" 18 | on: 19 | tags: true 20 | skip_cleanup: true 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Artem Morozov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Tarantool database backend 2 | 3 | [![Build Status](https://travis-ci.com/artembo/django-tarantool.svg?branch=master)](https://travis-ci.com/artembo/django-tarantool) 4 | [![Build Status](https://img.shields.io/pypi/v/django-tarantool.svg?color=blue)](https://pypi.org/project/django-tarantool/) 5 | 6 | [![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-tarantool?color=blue&label=%20&logo=django)](https://www.djangoproject.com/) 7 | 8 | ## Installation 9 | 10 | Install Tarantool v2.2+. See the installation manual for your OS [here](https://www.tarantool.io/en/download/) 11 | 12 | Make a database directory and run Tarantool instance there: 13 | 14 | ```bash 15 | $ mkdir ~/project_db 16 | $ cd ~/project_db 17 | $ tarantool 18 | ``` 19 | 20 | You will see the Tarantool interpreter. Initialize DB configuration and create password for *admin* 21 | 22 | ``` 23 | tarantool> box.cfg({ listen = 3301 }) 24 | tarantool> box.schema.user.passwd('admin', 'password') 25 | ``` 26 | 27 | To get started with django-tarantool, run the following in a virtual environment: 28 | 29 | ```bash 30 | pip install django-tarantool 31 | ``` 32 | 33 | Add ``DATABASES`` config of your Tarantool into ``settings.py`` 34 | 35 | ```python 36 | DATABASES = { 37 | 'default': { 38 | 'ENGINE': 'django_tarantool.backend', 39 | 'HOST': '127.0.0.1', 40 | 'PORT': '3301', 41 | 'USER': 'admin', 42 | 'PASSWORD': 'password', 43 | 'CONN_MAX_AGE': 3600, 44 | } 45 | } 46 | ``` 47 | 48 | Mind using *CONN_MAX_AGE* param as very important. It allows to keep connection opened for the specified time in 49 | seconds. Otherwise, Django will open the connection to the Tarantool instance on each request and close after it, which 50 | increases the request latency. 51 | 52 | Run `migrate` as usual: 53 | 54 | ```bash 55 | python manage.py migrate 56 | ``` 57 | 58 | Run Django development server: 59 | 60 | ```bash 61 | python manage.py runserver 0:8000 62 | ``` 63 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /django_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_app/__init__.py -------------------------------------------------------------------------------- /django_app/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_app/app/__init__.py -------------------------------------------------------------------------------- /django_app/app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from . import models 4 | 5 | 6 | admin.site.register(models.AllModel) 7 | admin.site.register(models.Float) 8 | admin.site.register(models.Decimal) 9 | admin.site.register(models.BigInteger) 10 | admin.site.register(models.Integer) 11 | admin.site.register(models.SmallInteger) 12 | admin.site.register(models.PositiveInteger) 13 | admin.site.register(models.PositiveSmallInteger) 14 | admin.site.register(models.Boolean) 15 | admin.site.register(models.NullBoolean) 16 | 17 | @admin.register(models.DateTime) 18 | class DateTimeAdmin(admin.ModelAdmin): 19 | readonly_fields = ('date_time_auto',) 20 | 21 | admin.site.register(models.Date) 22 | admin.site.register(models.Time) 23 | 24 | 25 | @admin.register(models.Char) 26 | class CharAdmin(admin.ModelAdmin): 27 | list_per_page = 5 28 | 29 | 30 | admin.site.register(models.Text) 31 | admin.site.register(models.GenericIPAddress) 32 | admin.site.register(models.File) 33 | admin.site.register(models.FilePath) 34 | admin.site.register(models.OneToOne) 35 | 36 | 37 | @admin.register(models.OneToOneRelative) 38 | class OneToOneRelative(admin.ModelAdmin): 39 | readonly_fields = ('id',) 40 | -------------------------------------------------------------------------------- /django_app/app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfig(AppConfig): 5 | name = 'app' 6 | -------------------------------------------------------------------------------- /django_app/app/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_app/app/management/__init__.py -------------------------------------------------------------------------------- /django_app/app/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_app/app/management/commands/__init__.py -------------------------------------------------------------------------------- /django_app/app/management/commands/perf_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.management import BaseCommand 4 | from model_bakery import baker 5 | 6 | from ...models import * 7 | 8 | ITERATIONS = 10000 9 | 10 | 11 | class Command(BaseCommand): 12 | 13 | def handle(self, *args, **options): 14 | t = datetime.datetime.now() 15 | # models_list = [ForeignKey, Text, Integer, Char, Float, GenericIPAddress, Uuid, Slug, DateTime, Date, Time] 16 | # for model in models_list: 17 | # baker.make(AllModel, ITERATIONS) 18 | for _ in range(30000): 19 | Integer.objects.create(integer=123123) 20 | dt = datetime.datetime.now() - t 21 | 22 | print(f"Insertion took {dt}") -------------------------------------------------------------------------------- /django_app/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-27 09:22 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='BigInteger', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('big_integer', models.BigIntegerField()), 21 | ('integer', models.IntegerField()), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Binary', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('binary', models.BinaryField()), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name='Boolean', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('boolean', models.BooleanField(default=False, verbose_name='Bool value')), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Char', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('char', models.CharField(max_length=100)), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='Date', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('date', models.DateField()), 50 | ], 51 | ), 52 | migrations.CreateModel( 53 | name='DateTime', 54 | fields=[ 55 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 56 | ('date_time', models.DateTimeField(blank=True, null=True)), 57 | ('date_time_auto', models.DateTimeField(auto_now_add=True)), 58 | ], 59 | ), 60 | migrations.CreateModel( 61 | name='Decimal', 62 | fields=[ 63 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 64 | ('decimal', models.DecimalField(decimal_places=2, max_digits=5)), 65 | ], 66 | ), 67 | migrations.CreateModel( 68 | name='Duration', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('duration', models.DurationField()), 72 | ], 73 | ), 74 | migrations.CreateModel( 75 | name='File', 76 | fields=[ 77 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 78 | ('file', models.FileField(upload_to='')), 79 | ], 80 | ), 81 | migrations.CreateModel( 82 | name='FilePath', 83 | fields=[ 84 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 85 | ('filepath', models.FilePathField(path='/Users/artem.morozov/projects/django-tarantool')), 86 | ], 87 | ), 88 | migrations.CreateModel( 89 | name='Float', 90 | fields=[ 91 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 92 | ('float', models.FloatField()), 93 | ], 94 | ), 95 | migrations.CreateModel( 96 | name='GenericIPAddress', 97 | fields=[ 98 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 99 | ('ip_address_v4', models.GenericIPAddressField(protocol='ipv4')), 100 | ('ip_address_v6', models.GenericIPAddressField(protocol='ipv6')), 101 | ('generic_ip_address', models.GenericIPAddressField()), 102 | ], 103 | ), 104 | migrations.CreateModel( 105 | name='Integer', 106 | fields=[ 107 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 108 | ('integer', models.IntegerField()), 109 | ], 110 | ), 111 | migrations.CreateModel( 112 | name='M2MDependency', 113 | fields=[ 114 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 115 | ], 116 | ), 117 | migrations.CreateModel( 118 | name='NullBoolean', 119 | fields=[ 120 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 121 | ('null_boolean', models.NullBooleanField(verbose_name='Null bool value')), 122 | ], 123 | ), 124 | migrations.CreateModel( 125 | name='OneToOneRelative', 126 | fields=[ 127 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 128 | ], 129 | ), 130 | migrations.CreateModel( 131 | name='PositiveInteger', 132 | fields=[ 133 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 134 | ('positive_integer', models.PositiveIntegerField()), 135 | ], 136 | ), 137 | migrations.CreateModel( 138 | name='PositiveSmallInteger', 139 | fields=[ 140 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 141 | ('positive_small_integer', models.PositiveSmallIntegerField()), 142 | ], 143 | ), 144 | migrations.CreateModel( 145 | name='Slug', 146 | fields=[ 147 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 148 | ('slug', models.SlugField()), 149 | ], 150 | ), 151 | migrations.CreateModel( 152 | name='SmallInteger', 153 | fields=[ 154 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 155 | ('small_integer', models.SmallIntegerField()), 156 | ], 157 | ), 158 | migrations.CreateModel( 159 | name='Text', 160 | fields=[ 161 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 162 | ('text', models.TextField()), 163 | ], 164 | ), 165 | migrations.CreateModel( 166 | name='Time', 167 | fields=[ 168 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 169 | ('time', models.TimeField()), 170 | ], 171 | ), 172 | migrations.CreateModel( 173 | name='Uuid', 174 | fields=[ 175 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 176 | ('uuid', models.UUIDField()), 177 | ], 178 | ), 179 | migrations.CreateModel( 180 | name='OneToOne', 181 | fields=[ 182 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 183 | ('one_to_one', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='one', to='app.OneToOneRelative')), 184 | ], 185 | ), 186 | migrations.CreateModel( 187 | name='ManyToMany', 188 | fields=[ 189 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 190 | ('name', models.CharField(max_length=20)), 191 | ('m2m', models.ManyToManyField(blank=True, to='app.M2MDependency')), 192 | ], 193 | ), 194 | migrations.CreateModel( 195 | name='ForeignKey', 196 | fields=[ 197 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 198 | ('foreign_key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.Integer')), 199 | ], 200 | ), 201 | migrations.CreateModel( 202 | name='AllModel', 203 | fields=[ 204 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 205 | ('boolean', models.BooleanField(default=True)), 206 | ('char', models.CharField(max_length=1)), 207 | ('decimal', models.DecimalField(decimal_places=2, max_digits=5)), 208 | ('file', models.FileField(upload_to='')), 209 | ('filepath', models.FilePathField(path='/Users/artem.morozov/projects/django-tarantool')), 210 | ('float', models.FloatField(null=True)), 211 | ('integer', models.IntegerField(null=True)), 212 | ('big_integer', models.BigIntegerField(null=True)), 213 | ('generic_ip_address', models.GenericIPAddressField()), 214 | ('null_boolean', models.NullBooleanField()), 215 | ('positive_integer', models.PositiveIntegerField()), 216 | ('positive_small_integer', models.PositiveSmallIntegerField()), 217 | ('slug', models.SlugField()), 218 | ('small_integer', models.SmallIntegerField()), 219 | ('text', models.TextField(null=True)), 220 | ('time', models.TimeField()), 221 | ('uuid', models.UUIDField(default=uuid.uuid4)), 222 | ('fk', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='all_models', to='app.Integer')), 223 | ('one_to_one', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='all_model', to='app.Integer')), 224 | ], 225 | ), 226 | ] 227 | -------------------------------------------------------------------------------- /django_app/app/migrations/0002_remove_biginteger_integer.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-27 09:23 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='biginteger', 15 | name='integer', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_app/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_app/app/migrations/__init__.py -------------------------------------------------------------------------------- /django_app/app/models.py: -------------------------------------------------------------------------------- 1 | import uuid as uuid 2 | from django.conf import settings 3 | from django.db import models 4 | 5 | 6 | class Boolean(models.Model): 7 | boolean = models.BooleanField('Bool value', default=False) 8 | 9 | def __str__(self): return str(self.boolean) 10 | 11 | 12 | class NullBoolean(models.Model): 13 | null_boolean = models.NullBooleanField('Null bool value') 14 | 15 | def __str__(self): return str(self.null_boolean) 16 | 17 | 18 | class File(models.Model): 19 | file = models.FileField() 20 | 21 | def __str__(self): return self.file.path if self.file else None 22 | 23 | 24 | class FilePath(models.Model): 25 | filepath = models.FilePathField(path=settings.BASE_DIR) 26 | 27 | def __str__(self): return self.filepath 28 | 29 | 30 | class Float(models.Model): 31 | float = models.FloatField() 32 | 33 | def __str__(self): return str(self.float) 34 | 35 | 36 | class Decimal(models.Model): 37 | decimal = models.DecimalField(max_digits=5, decimal_places=2) 38 | 39 | def __str__(self): return str(self.decimal) 40 | 41 | 42 | class Integer(models.Model): 43 | integer = models.IntegerField() 44 | 45 | def __str__(self): return str(self.integer) 46 | 47 | 48 | class BigInteger(models.Model): 49 | big_integer = models.BigIntegerField() 50 | 51 | def __str__(self): return str(self.big_integer) 52 | 53 | 54 | class PositiveInteger(models.Model): 55 | positive_integer = models.PositiveIntegerField() 56 | 57 | def __str__(self): return str(self.positive_integer) 58 | 59 | 60 | class SmallInteger(models.Model): 61 | small_integer = models.SmallIntegerField() 62 | 63 | def __str__(self): return str(self.small_integer) 64 | 65 | 66 | class PositiveSmallInteger(models.Model): 67 | positive_small_integer = models.PositiveSmallIntegerField() 68 | 69 | def __str__(self): return str(self.positive_small_integer) 70 | 71 | 72 | class OneToOneRelative(models.Model): 73 | 74 | def __str__(self): return str(self.pk) 75 | 76 | 77 | class OneToOne(models.Model): 78 | one_to_one = models.OneToOneField(OneToOneRelative, models.CASCADE, related_name='one') 79 | 80 | def __str__(self): return str(self.pk) 81 | 82 | 83 | class ForeignKey(models.Model): 84 | foreign_key = models.ForeignKey('app.Integer', on_delete=models.CASCADE) 85 | 86 | def __str__(self): return str(self.pk) 87 | 88 | 89 | class M2MDependency(models.Model): 90 | pass 91 | 92 | 93 | class ManyToMany(models.Model): 94 | m2m = models.ManyToManyField('app.M2MDependency', blank=True) 95 | name = models.CharField(max_length=20) 96 | 97 | def __str__(self): return self.name 98 | 99 | 100 | class GenericIPAddress(models.Model): 101 | ip_address_v4 = models.GenericIPAddressField(protocol='ipv4') 102 | ip_address_v6 = models.GenericIPAddressField(protocol='ipv6') 103 | generic_ip_address = models.GenericIPAddressField() 104 | 105 | def __str__(self): return self.generic_ip_address 106 | 107 | 108 | class Char(models.Model): 109 | char = models.CharField(max_length=100) 110 | 111 | def __str__(self): return self.char 112 | 113 | 114 | class Text(models.Model): 115 | text = models.TextField() 116 | 117 | def __str__(self): return self.text[:50] 118 | 119 | 120 | class Uuid(models.Model): 121 | uuid = models.UUIDField() 122 | 123 | def __str__(self): return str(self.uuid) 124 | 125 | 126 | class Slug(models.Model): 127 | slug = models.SlugField() 128 | 129 | def __str__(self): return self.slug 130 | 131 | 132 | class DateTime(models.Model): 133 | date_time = models.DateTimeField(null=True, blank=True) 134 | date_time_auto = models.DateTimeField(auto_now_add=True) 135 | 136 | def __str__(self): return str(self.date_time) 137 | 138 | 139 | class Date(models.Model): 140 | date = models.DateField() 141 | 142 | def __str__(self): return str(self.date) 143 | 144 | 145 | class Time(models.Model): 146 | time = models.TimeField() 147 | 148 | def __str__(self): return str(self.time) 149 | 150 | 151 | class Duration(models.Model): 152 | duration = models.DurationField() 153 | 154 | def __str__(self): return str(self.duration) 155 | 156 | 157 | class Binary(models.Model): 158 | binary = models.BinaryField() 159 | 160 | def __str__(self): return str(self.pk) 161 | 162 | 163 | class AllModel(models.Model): 164 | boolean = models.BooleanField(default=True) 165 | char = models.CharField(max_length=1) 166 | decimal = models.DecimalField(max_digits=5, decimal_places=2) 167 | file = models.FileField() 168 | filepath = models.FilePathField(path=settings.BASE_DIR) 169 | float = models.FloatField(null=True) 170 | integer = models.IntegerField(null=True) 171 | big_integer = models.BigIntegerField(null=True) 172 | generic_ip_address = models.GenericIPAddressField() 173 | null_boolean = models.NullBooleanField() 174 | one_to_one = models.OneToOneField('app.Integer', models.CASCADE, related_name='all_model') 175 | fk = models.OneToOneField('app.Integer', models.CASCADE, related_name='all_models') 176 | positive_integer = models.PositiveIntegerField() 177 | positive_small_integer = models.PositiveSmallIntegerField() 178 | slug = models.SlugField() 179 | small_integer = models.SmallIntegerField() 180 | text = models.TextField(null=True) 181 | time = models.TimeField() 182 | uuid = models.UUIDField(default=uuid.uuid4) 183 | -------------------------------------------------------------------------------- /django_app/app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .boolean import * 2 | from .null_boolean import * 3 | from .datetime import * 4 | from .time import * 5 | from .date import * 6 | from .float import * 7 | from .decimal import * 8 | from .integer import * 9 | from .big_integer import * 10 | from .positive_integer import * 11 | from .small_integer import * 12 | from .positive_small_integer import * 13 | from .char import * 14 | from .text import * 15 | from .uuid import * 16 | from .slug import * 17 | from .one_to_one import * 18 | from .foreign_key import * 19 | from .binary import * 20 | from .many_to_many import * 21 | from .generic_ip_address import * 22 | -------------------------------------------------------------------------------- /django_app/app/tests/big_integer.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import BigInteger 5 | 6 | 7 | class BigIntegerTests(TestCase): 8 | def setUp(self): 9 | self.int0_id = BigInteger.objects.create(big_integer=0).id 10 | self.int1_id = BigInteger.objects.create(big_integer=1111).id 11 | 12 | def test_create_integer(self): 13 | int0 = BigInteger.objects.get(id=self.int0_id) 14 | int1 = BigInteger.objects.get(id=self.int1_id) 15 | self.assertEqual(int0.big_integer, 0) 16 | self.assertEqual(int1.big_integer, 1111) 17 | self.assertLess(int0.big_integer, int1.big_integer) 18 | self.assertGreater(int1.big_integer, int0.big_integer) 19 | 20 | def test_extremal_values(self): 21 | int_biggest = BigInteger.objects.create(big_integer=18446744073709551615) 22 | self.assertEqual(int_biggest.big_integer, 18446744073709551615) 23 | int_smallest = BigInteger.objects.create(big_integer=-9223372036854775808) 24 | self.assertEqual(int_smallest.big_integer, -9223372036854775808) 25 | self.assertLess(int_smallest.big_integer, int_biggest.big_integer) 26 | with self.assertRaises(Exception): 27 | BigInteger.objects.create(big_integer=18446744073709551616) 28 | with self.assertRaises(Exception): 29 | BigInteger.objects.create(big_integer=-9223372036854776840) 30 | -------------------------------------------------------------------------------- /django_app/app/tests/binary.py: -------------------------------------------------------------------------------- 1 | # from io import BytesIO 2 | # 3 | # from django.core.exceptions import ValidationError 4 | # from django.test import TestCase 5 | # 6 | # from app.models import Binary 7 | # from django.db import DatabaseError 8 | # 9 | # 10 | # class BinaryTests(TestCase): 11 | # 12 | # @classmethod 13 | # def setUpClass(cls): 14 | # # cls.bytes = BytesIO(b'Binary content') 15 | # test_string = "Binary content" 16 | # res = bytes(test_string, 'utf-8') 17 | # Binary.objects.create(binary=res) 18 | # 19 | # def test_binary_content(self): 20 | # binary = Binary.objects.get() 21 | # print(binary) 22 | -------------------------------------------------------------------------------- /django_app/app/tests/boolean.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.test import TestCase 3 | 4 | from app.models import Boolean 5 | from django.db import DatabaseError 6 | 7 | 8 | class BooleanTests(TestCase): 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | Boolean.objects.create(boolean=True) 13 | Boolean.objects.create(boolean=False) 14 | 15 | def test_boolean_create(self): 16 | true = Boolean.objects.get(boolean=True) 17 | false = Boolean.objects.get(boolean=False) 18 | self.assertTrue(true.boolean) 19 | self.assertFalse(false.boolean) 20 | 21 | def test_boolean_exact_lookup(self): 22 | self.assertTrue(Boolean.objects.filter(boolean__exact=True).exists()) 23 | 24 | def test_boolean_in_lookup(self): 25 | self.assertEqual(Boolean.objects.filter(boolean__in=[True, False]).count(), 2) 26 | 27 | def test_boolean_isnull_lookup(self): 28 | self.assertEqual(Boolean.objects.filter(boolean__isnull=False).count(), 2) 29 | 30 | def test_boolean_incorrect_values(self): 31 | with self.assertRaises(ValidationError): 32 | Boolean.objects.create(boolean='string value') 33 | with self.assertRaises(DatabaseError): # Why DatabaseError? 34 | Boolean.objects.create(boolean=None) 35 | with self.assertRaises(ValidationError): 36 | Boolean.objects.create(boolean=100) -------------------------------------------------------------------------------- /django_app/app/tests/char.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import lorem_ipsum 3 | 4 | from app.models import Char 5 | 6 | 7 | class CharTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | char_value = lorem_ipsum.words(5) 11 | cls.char_value = char_value 12 | Char.objects.create(char=char_value) 13 | cls.end = ' '.join(char_value.split()[-2:]) 14 | 15 | def test_char_get(self): 16 | char_item = Char.objects.get(char=self.char_value) 17 | self.assertEqual(char_item.char, self.char_value) 18 | with self.assertRaises(Char.DoesNotExist): 19 | Char.objects.get(char='string which is not in the char') 20 | 21 | def test_char_contains(self): 22 | self.assertTrue(Char.objects.filter(char__contains='lorem').exists()) 23 | self.assertFalse(Char.objects.filter(char__contains='string which is not in the char').exists()) 24 | 25 | def test_char_icontains(self): 26 | self.assertTrue(Char.objects.filter(char__icontains='lOrEm').exists()) 27 | self.assertFalse(Char.objects.filter(char__icontains='string which is not in the char').exists()) 28 | 29 | def test_char_exact(self): 30 | self.assertTrue(Char.objects.filter(char__exact=self.char_value).exists()) 31 | self.assertFalse(Char.objects.filter(char__exact='string which is not in the char').exists()) 32 | 33 | def test_char_iexact(self): 34 | self.assertTrue(Char.objects.filter(char__iexact=self.char_value.upper()).exists()) 35 | self.assertFalse(Char.objects.filter(char__iexact='string which is not in the char').exists()) 36 | 37 | def test_char_startswith(self): 38 | self.assertTrue(Char.objects.filter(char__startswith='lorem ipsum').exists()) 39 | self.assertFalse(Char.objects.filter(char__startswith='string which is not in the char').exists()) 40 | 41 | def test_char_istartswith(self): 42 | self.assertTrue(Char.objects.filter(char__istartswith='lOreM iPSUm').exists()) 43 | self.assertFalse(Char.objects.filter(char__istartswith='string which is not in the char').exists()) 44 | 45 | def test_char_endswith(self): 46 | self.assertTrue(Char.objects.filter(char__endswith=self.end).exists()) 47 | self.assertFalse(Char.objects.filter(char__endswith='string which is not in the char').exists()) 48 | 49 | def test_char_iendswith(self): 50 | self.assertTrue(Char.objects.filter(char__iendswith=self.end.upper()).exists()) 51 | self.assertFalse(Char.objects.filter(char__iendswith='string which is not in the char').exists()) 52 | -------------------------------------------------------------------------------- /django_app/app/tests/date.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.test import TestCase 4 | from django.utils import timezone 5 | 6 | from app.models import Date 7 | 8 | 9 | class DateTests(TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.date_value = timezone.now().date() 13 | cls.date_in_the_future = cls.date_value + timedelta(days=10) 14 | cls.date_in_the_past = cls.date_value - timedelta(days=10) 15 | Date.objects.create(date=cls.date_value) 16 | Date.objects.create(date=cls.date_in_the_future) 17 | Date.objects.create(date=cls.date_in_the_past) 18 | 19 | def test_date(self): 20 | date = Date.objects.get(date=self.date_value) 21 | self.assertEqual(self.date_value, date.date) 22 | 23 | def test_date_lt(self): 24 | dates = Date.objects.filter(date__lt=self.date_in_the_future) 25 | self.assertEqual(dates.count(), 2) 26 | 27 | def test_date_gt(self): 28 | dates = Date.objects.filter(date__gt=self.date_in_the_past) 29 | self.assertEqual(dates.count(), 2) 30 | 31 | def test_date_gte(self): 32 | dates = Date.objects.filter(date__gte=self.date_value) 33 | self.assertEqual(dates.count(), 2) 34 | 35 | def test_date_lte(self): 36 | dates = Date.objects.filter(date__gte=self.date_value) 37 | self.assertEqual(dates.count(), 2) 38 | -------------------------------------------------------------------------------- /django_app/app/tests/datetime.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import timezone 3 | 4 | from app.models import DateTime 5 | 6 | 7 | class DateTimeTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | cls.beginning = timezone.now() 11 | DateTime.objects.create(date_time=cls.beginning) 12 | 13 | def test_date_time(self): 14 | date_time = DateTime.objects.get() 15 | self.assertEqual(date_time.date_time, self.beginning) 16 | 17 | now = timezone.now() 18 | DateTime.objects.create(date_time=now) 19 | latest = DateTime.objects.latest('date_time') 20 | self.assertEqual(now, latest.date_time) 21 | self.assertGreater(now, date_time.date_time) 22 | -------------------------------------------------------------------------------- /django_app/app/tests/decimal.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | from django.test import TestCase 4 | 5 | from app.models import Decimal 6 | 7 | 8 | class DecimalTests(TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | cls.decimal_value = 55.33 12 | cls.decimal_item_id = Decimal.objects.create(decimal=cls.decimal_value).id 13 | 14 | def test_char_create(self): 15 | decimal_item = Decimal.objects.get(id=self.decimal_item_id) 16 | self.assertEqual(decimal_item.decimal, self.decimal_value) 17 | 18 | def test_invalid_decimal(self): 19 | decimal_invalid_value = 55555.11111 20 | with self.assertRaises(decimal.InvalidOperation): 21 | Decimal.objects.create(decimal=decimal_invalid_value) -------------------------------------------------------------------------------- /django_app/app/tests/float.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import Float 5 | 6 | 7 | class FloatTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | cls.float_value = 44.66 11 | cls.float_item_id = Float.objects.create(float=cls.float_value).id 12 | 13 | def test_float_create(self): 14 | float_item = Float.objects.get(id=self.float_item_id) 15 | self.assertEqual(float_item.float, self.float_value) 16 | 17 | def test_invalid_float(self): 18 | with self.assertRaises(ValueError): 19 | Float.objects.create(float='string value') 20 | with self.assertRaises(DatabaseError): # Why DatabaseError? 21 | Float.objects.create(float=None) 22 | -------------------------------------------------------------------------------- /django_app/app/tests/foreign_key.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from django.test import TestCase 4 | 5 | from app.models import ForeignKey, Integer 6 | 7 | 8 | class ForeignKeyTests(TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | cls.random_int = randint(1, 1000000) 12 | integer = Integer.objects.create(integer=cls.random_int) 13 | for _ in range(10): 14 | ForeignKey.objects.create(foreign_key=integer) 15 | 16 | def test_foreign_key(self): 17 | integer = Integer.objects.get(integer=self.random_int) 18 | fk = ForeignKey.objects.filter(foreign_key=integer)[0] 19 | self.assertEqual(fk.foreign_key, integer) 20 | 21 | def test_foreign_key_count(self): 22 | integer = Integer.objects.get(integer=self.random_int) 23 | self.assertEqual(ForeignKey.objects.count(), integer.foreignkey_set.count()) 24 | -------------------------------------------------------------------------------- /django_app/app/tests/generic_ip_address.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from app.models import GenericIPAddress 4 | 5 | 6 | class ForeignKeyTests(TestCase): 7 | 8 | def test_create_ip_addresses(self): 9 | GenericIPAddress.objects.create( 10 | ip_address_v4='127.0.0.1', 11 | ip_address_v6='2001:0db8:0000:0000:0000:ff00:0042:8329', 12 | generic_ip_address='192.168.1.1', 13 | ) 14 | self.assertEqual( 15 | GenericIPAddress.objects.get(ip_address_v4='127.0.0.1').ip_address_v4, 16 | '127.0.0.1') 17 | self.assertEqual( 18 | GenericIPAddress.objects.get(ip_address_v6='2001:0db8:0000:0000:0000:ff00:0042:8329').ip_address_v6, 19 | '2001:db8::ff00:42:8329') 20 | self.assertEqual( 21 | GenericIPAddress.objects.get(generic_ip_address='192.168.1.1').generic_ip_address, 22 | '192.168.1.1') 23 | 24 | GenericIPAddress.objects.create( 25 | ip_address_v4='0.0.0.0', 26 | ip_address_v6='2001:db8:0:0:0:ff00:42:8339', 27 | generic_ip_address='255.255.128.0', 28 | ) 29 | self.assertEqual( 30 | GenericIPAddress.objects.get(ip_address_v4='0.0.0.0').ip_address_v4, 31 | '0.0.0.0') 32 | self.assertEqual( 33 | GenericIPAddress.objects.get(ip_address_v6='2001:db8:0:0:0:ff00:42:8339').ip_address_v6, 34 | '2001:db8::ff00:42:8339') 35 | self.assertEqual( 36 | GenericIPAddress.objects.get(generic_ip_address='255.255.128.0').generic_ip_address, 37 | '255.255.128.0') 38 | 39 | GenericIPAddress.objects.create( 40 | ip_address_v4='255.255.255.255', 41 | ip_address_v6='2001:4860:4861::8888', 42 | generic_ip_address='172.16.10.10', 43 | ) 44 | self.assertEqual( 45 | GenericIPAddress.objects.get(ip_address_v4='255.255.255.255').ip_address_v4, 46 | '255.255.255.255') 47 | self.assertEqual( 48 | GenericIPAddress.objects.get(ip_address_v6='2001:4860:4861::8888').ip_address_v6, 49 | '2001:4860:4861::8888') 50 | self.assertEqual( 51 | GenericIPAddress.objects.get(generic_ip_address='172.16.10.10').generic_ip_address, 52 | '172.16.10.10') 53 | -------------------------------------------------------------------------------- /django_app/app/tests/integer.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import Integer 5 | 6 | 7 | class IntegerTests(TestCase): 8 | def setUp(self): 9 | self.int0_id = Integer.objects.create(integer=0).id 10 | self.int1_id = Integer.objects.create(integer=1111).id 11 | 12 | def test_create_integer(self): 13 | int0 = Integer.objects.get(id=self.int0_id) 14 | int1 = Integer.objects.get(id=self.int1_id) 15 | self.assertEqual(int0.integer, 0) 16 | self.assertEqual(int1.integer, 1111) 17 | self.assertLess(int0.integer, int1.integer) 18 | self.assertGreater(int1.integer, int0.integer) 19 | 20 | def test_extremal_values(self): 21 | int_biggest = Integer.objects.create(integer=18446744073709551615) 22 | self.assertEqual(int_biggest.integer, 18446744073709551615) 23 | int_smallest = Integer.objects.create(integer=-9223372036854775808) 24 | self.assertEqual(int_smallest.integer, -9223372036854775808) 25 | self.assertLess(int_smallest.integer, int_biggest.integer) 26 | with self.assertRaises(Exception): 27 | Integer.objects.create(integer=18446744073709551616) 28 | with self.assertRaises(Exception): 29 | Integer.objects.create(integer=-9223372036854776840) 30 | -------------------------------------------------------------------------------- /django_app/app/tests/many_to_many.py: -------------------------------------------------------------------------------- 1 | # from django.db import DatabaseError 2 | # from django.test import TestCase 3 | # 4 | # from app.models import ManyToMany, ForeignKey 5 | # 6 | # 7 | # class ManyToManyTests(TestCase): 8 | # @classmethod 9 | # def setUpClass(cls): 10 | # for _ in range(10): 11 | # fk = ForeignKey.objects.create() 12 | # m2m = ManyToMany.objects.create(name='m2m_initial') 13 | -------------------------------------------------------------------------------- /django_app/app/tests/null_boolean.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.test import TestCase 3 | 4 | from app.models import NullBoolean 5 | 6 | 7 | class NullBooleanTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | NullBoolean.objects.create(null_boolean=True) 11 | NullBoolean.objects.create(null_boolean=False) 12 | NullBoolean.objects.create(null_boolean=None) 13 | 14 | def test_create_null_boolean(self): 15 | true = NullBoolean.objects.get(null_boolean=True) 16 | false = NullBoolean.objects.get(null_boolean=False) 17 | none = NullBoolean.objects.get(null_boolean=None) 18 | self.assertTrue(true.null_boolean) 19 | self.assertFalse(false.null_boolean) 20 | self.assertIsNone(none.null_boolean) 21 | 22 | def test_wrong_values(self): 23 | with self.assertRaises(ValidationError): 24 | NullBoolean.objects.create(null_boolean='string value') 25 | with self.assertRaises(ValidationError): 26 | NullBoolean.objects.create(null_boolean=100) 27 | -------------------------------------------------------------------------------- /django_app/app/tests/one_to_one.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import OneToOneRelative, OneToOne 5 | 6 | 7 | class OneToOneTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | for _ in range(10): 11 | relative = OneToOneRelative.objects.create() 12 | OneToOne.objects.create(one_to_one=relative) 13 | 14 | def test_one_to_one(self): 15 | relative_first = OneToOneRelative.objects.first() 16 | relative_last = OneToOneRelative.objects.last() 17 | relative_random = OneToOneRelative.objects.all().order_by('?')[0] 18 | self.assertEqual(relative_first.id, 1) 19 | self.assertEqual(relative_last.id, 10) 20 | self.assertEqual(relative_random.one.one_to_one_id, relative_random.pk) 21 | self.assertEqual(OneToOne.objects.count(), OneToOneRelative.objects.count()) 22 | 23 | def test_values_list(self): 24 | self.assertListEqual(list(OneToOne.objects.values_list('one_to_one', flat=True)), 25 | list(OneToOneRelative.objects.filter(one__isnull=False).values_list('one__id', flat=True))) 26 | 27 | def test_one_to_one_duplicate_relative(self): 28 | relative = OneToOneRelative.objects.first() 29 | with self.assertRaises(DatabaseError): 30 | OneToOne.objects.create(one_to_one=relative) 31 | 32 | def test_one_to_one_invalid(self): 33 | with self.assertRaises(ValueError): 34 | OneToOne.objects.create(one_to_one='string value') 35 | -------------------------------------------------------------------------------- /django_app/app/tests/positive_integer.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import PositiveInteger 5 | 6 | 7 | class PositiveIntegerTests(TestCase): 8 | def setUp(self): 9 | self.int0_id = PositiveInteger.objects.create(positive_integer=0).id 10 | self.int1_id = PositiveInteger.objects.create(positive_integer=1111).id 11 | 12 | def test_create_integer(self): 13 | int0 = PositiveInteger.objects.get(id=self.int0_id) 14 | int1 = PositiveInteger.objects.get(id=self.int1_id) 15 | self.assertEqual(int0.positive_integer, 0) 16 | self.assertEqual(int1.positive_integer, 1111) 17 | self.assertLess(int0.positive_integer, int1.positive_integer) 18 | self.assertGreater(int1.positive_integer, int0.positive_integer) 19 | 20 | def test_extremal_values(self): 21 | int_biggest = PositiveInteger.objects.create(positive_integer=18446744073709551615) 22 | self.assertEqual(int_biggest.positive_integer, 18446744073709551615) 23 | int_smallest = PositiveInteger.objects.create(positive_integer=0) 24 | self.assertEqual(int_smallest.positive_integer, 0) 25 | self.assertLess(int_smallest.positive_integer, int_biggest.positive_integer) 26 | with self.assertRaises(Exception): 27 | PositiveInteger.objects.create( 28 | positive_integer=18446744073709551616) 29 | with self.assertRaises(DatabaseError): 30 | PositiveInteger.objects.create(positive_integer=-1) 31 | -------------------------------------------------------------------------------- /django_app/app/tests/positive_small_integer.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import PositiveSmallInteger 5 | 6 | 7 | class PositiveSmallIntegerTests(TestCase): 8 | def setUp(self): 9 | self.int0_id = PositiveSmallInteger.objects.create(positive_small_integer=0).id 10 | self.int1_id = PositiveSmallInteger.objects.create(positive_small_integer=1111).id 11 | 12 | def test_create_integer(self): 13 | int0 = PositiveSmallInteger.objects.get(id=self.int0_id) 14 | int1 = PositiveSmallInteger.objects.get(id=self.int1_id) 15 | self.assertEqual(int0.positive_small_integer, 0) 16 | self.assertEqual(int1.positive_small_integer, 1111) 17 | self.assertLess(int0.positive_small_integer, int1.positive_small_integer) 18 | self.assertGreater(int1.positive_small_integer, int0.positive_small_integer) 19 | 20 | def test_extremal_values(self): 21 | int_biggest = PositiveSmallInteger.objects.create(positive_small_integer=18446744073709551615) 22 | self.assertEqual(int_biggest.positive_small_integer, 18446744073709551615) 23 | int_smallest = PositiveSmallInteger.objects.create(positive_small_integer=0) 24 | self.assertEqual(int_smallest.positive_small_integer, 0) 25 | self.assertLess(int_smallest.positive_small_integer, int_biggest.positive_small_integer) 26 | with self.assertRaises(Exception): 27 | PositiveSmallInteger.objects.create( 28 | positive_small_integer=18446744073709551616) 29 | with self.assertRaises(DatabaseError): 30 | PositiveSmallInteger.objects.create(positive_small_integer=-1) 31 | -------------------------------------------------------------------------------- /django_app/app/tests/slug.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import lorem_ipsum 3 | 4 | from app.models import Slug 5 | 6 | 7 | class SlugTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | slug_value = lorem_ipsum.words(100) 11 | cls.slug_value = slug_value 12 | Slug.objects.create(slug=slug_value) 13 | cls.end = ' '.join(slug_value.split()[-2:]) 14 | 15 | def test_slug_get(self): 16 | slug_item = Slug.objects.get(slug=self.slug_value) 17 | self.assertEqual(slug_item.slug, self.slug_value) 18 | with self.assertRaises(Slug.DoesNotExist): 19 | Slug.objects.get(slug='string which is not in the slug') 20 | 21 | def test_slug_contains(self): 22 | self.assertTrue(Slug.objects.filter(slug__contains='ipsum').exists()) 23 | self.assertTrue(Slug.objects.filter(slug__contains='lorem ipsum').exists()) 24 | self.assertFalse(Slug.objects.filter(slug__contains='string which is not in the slug').exists()) 25 | 26 | def test_slug_icontains(self): 27 | self.assertTrue(Slug.objects.filter(slug__icontains='IpSuM').exists()) 28 | self.assertFalse(Slug.objects.filter(slug__icontains='string which is not in the slug').exists()) 29 | 30 | def test_slug_exact(self): 31 | self.assertTrue(Slug.objects.filter(slug__exact=self.slug_value).exists()) 32 | self.assertFalse(Slug.objects.filter(slug__exact='string which is not in the slug').exists()) 33 | 34 | def test_slug_iexact(self): 35 | self.assertTrue(Slug.objects.filter(slug__iexact=self.slug_value.upper()).exists()) 36 | self.assertFalse(Slug.objects.filter(slug__iexact='string which is not in the slug').exists()) 37 | 38 | def test_slug_startswith(self): 39 | self.assertTrue(Slug.objects.filter(slug__startswith='lorem ipsum').exists()) 40 | self.assertFalse(Slug.objects.filter(slug__startswith='string which is not in the slug').exists()) 41 | 42 | def test_slug_istartswith(self): 43 | self.assertTrue(Slug.objects.filter(slug__istartswith='lOreM iPSUm').exists()) 44 | self.assertFalse(Slug.objects.filter(slug__istartswith='string which is not in the slug').exists()) 45 | 46 | def test_slug_endswith(self): 47 | self.assertTrue(Slug.objects.filter(slug__endswith=self.end).exists()) 48 | self.assertFalse(Slug.objects.filter(slug__endswith='string which is not in the slug').exists()) 49 | 50 | def test_slug_iendswith(self): 51 | self.assertTrue(Slug.objects.filter(slug__iendswith=self.end.upper()).exists()) 52 | self.assertFalse(Slug.objects.filter(slug__iendswith='string which is not in the slug').exists()) 53 | -------------------------------------------------------------------------------- /django_app/app/tests/small_integer.py: -------------------------------------------------------------------------------- 1 | from django.db import DatabaseError 2 | from django.test import TestCase 3 | 4 | from app.models import SmallInteger 5 | 6 | 7 | class SmallIntegerTests(TestCase): 8 | def setUp(self): 9 | self.int0_id = SmallInteger.objects.create(small_integer=0).id 10 | self.int1_id = SmallInteger.objects.create(small_integer=1111).id 11 | 12 | def test_create_integer(self): 13 | int0 = SmallInteger.objects.get(id=self.int0_id) 14 | int1 = SmallInteger.objects.get(id=self.int1_id) 15 | self.assertEqual(int0.small_integer, 0) 16 | self.assertEqual(int1.small_integer, 1111) 17 | self.assertLess(int0.small_integer, int1.small_integer) 18 | self.assertGreater(int1.small_integer, int0.small_integer) 19 | 20 | def test_extremal_values(self): 21 | int_biggest = SmallInteger.objects.create(small_integer=18446744073709551615) 22 | self.assertEqual(int_biggest.small_integer, 18446744073709551615) 23 | int_smallest = SmallInteger.objects.create(small_integer=-9223372036854775808) 24 | self.assertEqual(int_smallest.small_integer, -9223372036854775808) 25 | self.assertLess(int_smallest.small_integer, int_biggest.small_integer) 26 | with self.assertRaises(Exception): 27 | SmallInteger.objects.create(small_integer=18446744073709551616) 28 | with self.assertRaises(Exception): 29 | SmallInteger.objects.create(small_integer=-9223372036854776840) 30 | -------------------------------------------------------------------------------- /django_app/app/tests/text.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.utils import lorem_ipsum 3 | 4 | from app.models import Text 5 | 6 | 7 | class TextTests(TestCase): 8 | @classmethod 9 | def setUpClass(cls): 10 | text_value = lorem_ipsum.words(100) 11 | cls.text_value = text_value 12 | Text.objects.create(text=text_value) 13 | cls.end = ' '.join(text_value.split()[-2:]) 14 | 15 | def test_text_get(self): 16 | text_item = Text.objects.get(text=self.text_value) 17 | self.assertEqual(text_item.text, self.text_value) 18 | with self.assertRaises(Text.DoesNotExist): 19 | Text.objects.get(text='string which is not in the text') 20 | 21 | def test_text_contains(self): 22 | self.assertTrue(Text.objects.filter(text__contains='ipsum').exists()) 23 | self.assertFalse(Text.objects.filter(text__contains='string which is not in the text').exists()) 24 | 25 | def test_text_icontains(self): 26 | self.assertTrue(Text.objects.filter(text__icontains='IpSuM').exists()) 27 | self.assertFalse(Text.objects.filter(text__icontains='string which is not in the text').exists()) 28 | 29 | def test_text_exact(self): 30 | self.assertTrue(Text.objects.filter(text__exact=self.text_value).exists()) 31 | self.assertFalse(Text.objects.filter(text__exact='string which is not in the text').exists()) 32 | 33 | def test_text_iexact(self): 34 | self.assertTrue(Text.objects.filter(text__iexact=self.text_value.upper()).exists()) 35 | self.assertFalse(Text.objects.filter(text__iexact='string which is not in the text').exists()) 36 | 37 | def test_text_startswith(self): 38 | self.assertTrue(Text.objects.filter(text__startswith='lorem ipsum').exists()) 39 | self.assertFalse(Text.objects.filter(text__startswith='string which is not in the text').exists()) 40 | 41 | def test_text_istartswith(self): 42 | self.assertTrue(Text.objects.filter(text__istartswith='lOreM iPSUm').exists()) 43 | self.assertFalse(Text.objects.filter(text__istartswith='string which is not in the text').exists()) 44 | 45 | def test_text_endswith(self): 46 | self.assertTrue(Text.objects.filter(text__endswith=self.end).exists()) 47 | self.assertFalse(Text.objects.filter(text__endswith='string which is not in the text').exists()) 48 | 49 | def test_text_iendswith(self): 50 | self.assertTrue(Text.objects.filter(text__iendswith=self.end.upper()).exists()) 51 | self.assertFalse(Text.objects.filter(text__iendswith='string which is not in the text').exists()) 52 | -------------------------------------------------------------------------------- /django_app/app/tests/time.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | from django.utils import timezone 6 | 7 | from app.models import Time 8 | 9 | 10 | class TimeTests(TestCase): 11 | @classmethod 12 | def setUpClass(cls): 13 | cls.morning_time_value = datetime.time(hour=9, minute=20, second=10) 14 | cls.evening_time_value = datetime.time(hour=19, minute=2, second=55) 15 | cls.time_value = timezone.now().time().replace(microsecond=0) 16 | Time.objects.create(time=cls.time_value) 17 | Time.objects.create(time=cls.morning_time_value) 18 | Time.objects.create(time=cls.evening_time_value) 19 | 20 | def test_time_values(self): 21 | time_item = Time.objects.get(time=self.time_value) 22 | self.assertEqual(time_item.time, self.time_value) 23 | time_now = timezone.now().time() 24 | self.assertLess(time_item.time, time_now) 25 | self.assertGreater(time_now, time_item.time) 26 | 27 | def test_lookups(self): 28 | time_now = timezone.now().time() 29 | Time.objects.filter(time__lte=time_now) 30 | self.assertTrue(Time.objects.filter(time__lt=self.evening_time_value).exists()) 31 | self.assertTrue(Time.objects.filter(time__lte=self.evening_time_value).exists()) 32 | self.assertTrue(Time.objects.filter(time__gt=self.morning_time_value).exists()) 33 | self.assertTrue(Time.objects.filter(time__gte=self.morning_time_value).exists()) 34 | 35 | def test_time_invalid_value(self): 36 | with self.assertRaises(ValidationError): 37 | Time.objects.create(time='string value') 38 | -------------------------------------------------------------------------------- /django_app/app/tests/uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | 6 | from app.models import Uuid 7 | 8 | 9 | class UuidTests(TestCase): 10 | @classmethod 11 | def setUpTestData(cls): 12 | cls.uuid_value = uuid.uuid4() 13 | Uuid.objects.create(uuid=cls.uuid_value) 14 | 15 | def test_uuid(self): 16 | uuid_item = Uuid.objects.get(uuid=self.uuid_value) 17 | self.assertEqual(uuid_item.uuid, self.uuid_value) 18 | 19 | def test_uuid_exact(self): 20 | uuid_item = Uuid.objects.get(uuid__exact=self.uuid_value) 21 | self.assertEqual(uuid_item.uuid, self.uuid_value) 22 | 23 | def test_uuid_in(self): 24 | uuid_item = Uuid.objects.get(uuid__in=[self.uuid_value]) 25 | self.assertEqual(uuid_item.uuid, self.uuid_value) 26 | 27 | def test_invalid_uuid(self): 28 | with self.assertRaises(ValidationError): 29 | Uuid.objects.create(uuid='non-uuid text value') 30 | -------------------------------------------------------------------------------- /django_app/app/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def camel_to_snake(name): 5 | name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 6 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() 7 | -------------------------------------------------------------------------------- /django_app/app/views.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django.http import HttpResponse 3 | 4 | 5 | @transaction.atomic 6 | def index(request): 7 | return HttpResponse() 8 | -------------------------------------------------------------------------------- /django_app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | from settings import BASE_DIR 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | APP_DIR = os.path.join(BASE_DIR, 'django_app') 11 | 12 | sys.path.append(BASE_DIR) 13 | sys.path.append(APP_DIR) 14 | 15 | 16 | def main(): 17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') 18 | try: 19 | from django.core.management import execute_from_command_line 20 | except ImportError as exc: 21 | raise ImportError( 22 | "Couldn't import Django. Are you sure it's installed and " 23 | "available on your PYTHONPATH environment variable? Did you " 24 | "forget to activate a virtual environment?" 25 | ) from exc 26 | execute_from_command_line(sys.argv) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /django_app/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | 8 | # Quick-start development settings - unsuitable for production 9 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 10 | 11 | # SECURITY WARNING: keep the secret key used in production secret! 12 | SECRET_KEY = 'kdtvq!t_(#lke93^r!b(c1a&q!c64vp2$*3il3-+nw0^u8=k9q' 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = True 16 | 17 | ALLOWED_HOSTS = ['*'] 18 | 19 | INSTALLED_APPS = [ 20 | 'app', 21 | 'django.contrib.admin', 22 | 'django.contrib.auth', 23 | 'django.contrib.contenttypes', 24 | 'django.contrib.sessions', 25 | 'django.contrib.messages', 26 | 'django.contrib.staticfiles', 27 | ] 28 | 29 | MIDDLEWARE = [ 30 | 'django.middleware.security.SecurityMiddleware', 31 | 'django.contrib.sessions.middleware.SessionMiddleware', 32 | 'django.middleware.common.CommonMiddleware', 33 | 'django.middleware.csrf.CsrfViewMiddleware', 34 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 35 | 'django.contrib.messages.middleware.MessageMiddleware', 36 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 37 | ] 38 | 39 | ROOT_URLCONF = 'django_app.urls' 40 | 41 | TEMPLATES = [ 42 | { 43 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 44 | 'DIRS': [], 45 | 'APP_DIRS': True, 46 | 'OPTIONS': { 47 | 'context_processors': [ 48 | 'django.template.context_processors.debug', 49 | 'django.template.context_processors.request', 50 | 'django.contrib.auth.context_processors.auth', 51 | 'django.contrib.messages.context_processors.messages', 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | WSGI_APPLICATION = 'wsgi.application' 58 | 59 | 60 | # Database 61 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 62 | 63 | 64 | DB_T = { 65 | 'default': { 66 | 'ENGINE': 'django_tarantool.backend', 67 | 'HOST': '127.0.0.1', 68 | 'PORT': '3301', 69 | 'USER': 'admin', 70 | 'PASSWORD': 'password', 71 | 'CONN_MAX_AGE': 3600, 72 | } 73 | } 74 | 75 | DB_S = { 76 | 'default': { 77 | 'ENGINE': 'django.db.backends.sqlite3', 78 | 'NAME': os.path.join(BASE_DIR, 'sqlite.db'), 79 | } 80 | } 81 | 82 | DB_P = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 85 | 'NAME': 'postgres', 86 | 'USER': 'postgres', 87 | 'PASSWORD': 'parol', 88 | 'HOST': 'localhost', 89 | 'CONN_MAX_AGE': 3600, 90 | } 91 | } 92 | 93 | DATABASES = DB_T 94 | 95 | # Internationalization 96 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 97 | 98 | LANGUAGE_CODE = 'en-us' 99 | 100 | TIME_ZONE = 'Europe/Moscow' 101 | 102 | USE_I18N = True 103 | 104 | USE_L10N = True 105 | 106 | USE_TZ = True 107 | 108 | 109 | # Static files (CSS, JavaScript, Images) 110 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 111 | 112 | STATIC_URL = '/static/' 113 | 114 | 115 | LOGGING_TO_CONSOLE = { 116 | 'version': 1, 117 | 'filters': { 118 | 'require_debug_true': { 119 | '()': 'django.utils.log.RequireDebugTrue', 120 | } 121 | }, 122 | 'handlers': { 123 | 'console': { 124 | 'level': 'DEBUG', 125 | 'filters': ['require_debug_true'], 126 | 'class': 'logging.StreamHandler', 127 | } 128 | }, 129 | 'loggers': { 130 | 'django.db.backends': { 131 | 'level': 'DEBUG', 132 | 'handlers': ['console'], 133 | } 134 | } 135 | } 136 | 137 | # LOGGING = LOGGING_TO_CONSOLE 138 | -------------------------------------------------------------------------------- /django_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | from app.views import index 5 | 6 | urlpatterns = [ 7 | url(r'^admin/', admin.site.urls), 8 | url(r'^$', index) 9 | ] 10 | -------------------------------------------------------------------------------- /django_app/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_app.settings') 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /django_tarantool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_tarantool/__init__.py -------------------------------------------------------------------------------- /django_tarantool/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artembo/django-tarantool/34e9d24319f5036ee56d74bf3643447702e2bfea/django_tarantool/backend/__init__.py -------------------------------------------------------------------------------- /django_tarantool/backend/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool database backend for Django. 3 | """ 4 | 5 | 6 | from django.db.backends.base.base import BaseDatabaseWrapper 7 | from django.db.backends.base.validation import BaseDatabaseValidation 8 | from django.db.backends.utils import ( 9 | CursorDebugWrapper as BaseCursorDebugWrapper, 10 | ) 11 | 12 | from .client import DatabaseClient # NOQA isort:skip 13 | from .creation import DatabaseCreation # NOQA isort:skip 14 | from .features import DatabaseFeatures # NOQA isort:skip 15 | from .introspection import DatabaseIntrospection # NOQA isort:skip 16 | from .operations import DatabaseOperations # NOQA isort:skip 17 | from .schema import DatabaseSchemaEditor # NOQA isort:skip 18 | 19 | 20 | from tarantool import dbapi 21 | 22 | 23 | Database = dbapi 24 | 25 | 26 | class DatabaseWrapper(BaseDatabaseWrapper): 27 | def is_usable(self): 28 | return True 29 | 30 | vendor = 'tarantool' 31 | display_name = 'Tarantool' 32 | Database = Database 33 | SchemaEditorClass = DatabaseSchemaEditor 34 | client_class = DatabaseClient 35 | creation_class = DatabaseCreation 36 | features_class = DatabaseFeatures 37 | introspection_class = DatabaseIntrospection 38 | ops_class = DatabaseOperations 39 | 40 | data_types = { 41 | 'AutoField': 'INTEGER', 42 | 'BigAutoField': 'INTEGER', 43 | 'BinaryField': 'VARBINARY', 44 | 'BooleanField': 'BOOLEAN', 45 | 'NullBooleanField': 'BOOLEAN', 46 | 'CharField': 'VARCHAR(%(max_length)s)', 47 | 'TextField': 'TEXT', 48 | 'UUIDField': 'VARCHAR(32)', 49 | 'SlugField': 'VARCHAR(%(max_length)s)', 50 | 'DateField': 'UNSIGNED', 51 | 'DateTimeField': 'NUMBER', 52 | 'TimeField': 'UNSIGNED', 53 | 'DurationField': 'INTEGER', 54 | 'OneToOneField': 'INTEGER', 55 | 'FileField': 'VARCHAR(%(max_length)s)', 56 | 'FilePathField': 'VARCHAR(%(max_length)s)', 57 | 'BigIntegerField': 'INTEGER', 58 | 'IntegerField': 'INTEGER', 59 | 'PositiveIntegerField': 'UNSIGNED', 60 | 'SmallIntegerField': 'INTEGER', 61 | 'PositiveSmallIntegerField': 'UNSIGNED', 62 | 'FloatField': 'NUMBER', 63 | 'DecimalField': 'NUMBER', 64 | 'IPAddressField': 'VARCHAR(15)', 65 | 'GenericIPAddressField': 'VARCHAR(39)', 66 | } 67 | 68 | data_types_suffix = { 69 | 'AutoField': 'AUTOINCREMENT', 70 | 'BigAutoField': 'AUTOINCREMENT', 71 | } 72 | 73 | pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')" 74 | operators = { 75 | 'exact': '= %s', 76 | 'iexact': "= UPPER(%s)", 77 | 'contains': "LIKE %s", 78 | 'icontains': "LIKE UPPER(%s)", 79 | 'regex': 'REGEXP %s', 80 | 'iregex': "REGEXP '(?i)' || %s", 81 | 'gt': '> %s', 82 | 'gte': '>= %s', 83 | 'lt': '< %s', 84 | 'lte': '<= %s', 85 | 'startswith': "LIKE %s", 86 | 'endswith': "LIKE %s", 87 | 'istartswith': "LIKE UPPER(%s)", 88 | 'iendswith': "LIKE UPPER(%s)", 89 | } 90 | 91 | pattern_ops = { 92 | 'contains': r"LIKE '%%' || {} || '%%' ESCAPE '\'", 93 | 'icontains': r"LIKE '%%' || UPPER('{}') || '%%' ESCAPE '\'", 94 | 'startswith': r"LIKE {} || '%%' ESCAPE '\'", 95 | 'istartswith': r"LIKE UPPER({}) || '%%' ESCAPE '\'", 96 | 'endswith': r"LIKE '%%' || {} ESCAPE '\'", 97 | 'iendswith': r"LIKE '%%' || UPPER({}) ESCAPE '\'", 98 | } 99 | 100 | def __init__(self, *args, **kwargs): 101 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 102 | 103 | self.ops = DatabaseOperations(self) 104 | self.features = DatabaseFeatures(self) 105 | self.creation = DatabaseCreation(self) 106 | self.introspection = DatabaseIntrospection(self) 107 | self.validation = BaseDatabaseValidation(self) 108 | 109 | def get_connection_params(self): 110 | settings_dict = self.settings_dict 111 | 112 | conn_params = { 113 | **settings_dict['OPTIONS'], 114 | } 115 | conn_params.pop('isolation_level', None) 116 | if settings_dict['USER']: 117 | conn_params['user'] = settings_dict['USER'] 118 | if settings_dict['PASSWORD']: 119 | conn_params['password'] = settings_dict['PASSWORD'] 120 | if settings_dict['HOST']: 121 | conn_params['host'] = settings_dict['HOST'] 122 | if settings_dict['PORT']: 123 | conn_params['port'] = settings_dict['PORT'] 124 | conn_params['use_list'] = False 125 | return conn_params 126 | 127 | def get_new_connection(self, conn_params): 128 | connection = dbapi.connect(**conn_params) 129 | return connection 130 | 131 | def _set_autocommit(self, value): 132 | pass 133 | 134 | def init_connection_state(self): 135 | pass 136 | 137 | def create_cursor(self, name=None): 138 | return CursorWrapper(self.connection.cursor()) 139 | 140 | def make_debug_cursor(self, cursor): 141 | return CursorDebugWrapper(cursor, self) 142 | 143 | 144 | class CursorWrapper: 145 | """Tarantool supports only qmark and named param styles 146 | the easies way to send SQL query with proper for tarantool 147 | parameters is to convert them to qmark style. 148 | We need to overwrite execute and executemany methods to do it. 149 | """ 150 | def __init__(self, cursor): 151 | self.cursor = cursor 152 | 153 | def execute(self, query, params=()): 154 | query = self.convert_query(query, len(params)) if params else query 155 | return self.cursor.execute(query, params) 156 | 157 | def executemany(self, query, param_list): 158 | query = self.convert_query(query, len(param_list[0])) if param_list else query 159 | return self.cursor.executemany(query, param_list) 160 | 161 | def convert_query(self, query, num_params): 162 | return query % tuple("?" * num_params) 163 | 164 | def __getattr__(self, attr): 165 | return getattr(self.cursor, attr) 166 | 167 | def __iter__(self): 168 | return iter(self.cursor) 169 | 170 | 171 | class CursorDebugWrapper(BaseCursorDebugWrapper): 172 | def copy_expert(self, sql, file, *args): 173 | with self.debug_sql(sql): 174 | return self.cursor.copy_expert(sql, file, *args) 175 | 176 | def copy_to(self, file, table, *args, **kwargs): 177 | with self.debug_sql(sql='COPY %s TO STDOUT' % table): 178 | return self.cursor.copy_to(file, table, *args, **kwargs) 179 | -------------------------------------------------------------------------------- /django_tarantool/backend/client.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from django.db.backends.base.client import BaseDatabaseClient 4 | 5 | 6 | class DatabaseClient(BaseDatabaseClient): 7 | executable_name = 'tarantoolctl' 8 | 9 | @classmethod 10 | def settings_to_cmd_args(cls, settings_dict): 11 | args = [cls.executable_name] 12 | 13 | user = settings_dict['OPTIONS'].get('user', settings_dict['USER']) 14 | passwd = settings_dict['OPTIONS'].get('passwd', settings_dict['PASSWORD']) 15 | host = settings_dict['OPTIONS'].get('host', settings_dict['HOST']) 16 | port = settings_dict['OPTIONS'].get('port', settings_dict['PORT']) 17 | 18 | args += ["connect", "tcp://%s:%s@%s:%s" % (user, passwd, host, port)] 19 | return args 20 | 21 | def runshell(self): 22 | args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict) 23 | subprocess.run(args, check=True) 24 | 25 | -------------------------------------------------------------------------------- /django_tarantool/backend/creation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | from pathlib import Path 5 | 6 | from django.core.management import call_command 7 | from django.db.backends.base.creation import BaseDatabaseCreation 8 | 9 | from django_app import settings 10 | from django_tarantool.backend.utils import wait_for_tarantool 11 | 12 | START_LUA = """\ 13 | box.cfg({ listen = %d }) 14 | box.schema.user.passwd('admin', 'password') 15 | """ 16 | 17 | 18 | class DatabaseCreation(BaseDatabaseCreation): 19 | tarantool_proc = None 20 | tarantool_test_port = 3302 21 | db_path = None 22 | 23 | def _quote_name(self, name): 24 | return self.connection.ops.quote_name(name) 25 | 26 | def _prepare_db_path(self, db_name): 27 | path = os.path.join(settings.BASE_DIR, db_name) 28 | Path(path).mkdir(parents=True, exist_ok=True) 29 | with open(os.path.join(path, 'start.lua'), 'w+') as file: 30 | file.write(START_LUA % self.tarantool_test_port) 31 | return path 32 | 33 | def start_test_tarantool(self): 34 | test_database_name = self._get_test_db_name() 35 | self.db_path = self._prepare_db_path(test_database_name) 36 | self.tarantool_proc = subprocess.Popen(['tarantool', 'start.lua'], cwd=self.db_path) 37 | wait_for_tarantool(self.connection) 38 | 39 | def create_test_db(self, verbosity=1, autoclobber=False, serialize=True, keepdb=False): 40 | settings.DATABASES[self.connection.alias]["PORT"] = self.tarantool_test_port 41 | self.connection.settings_dict["PORT"] = self.tarantool_test_port 42 | self.start_test_tarantool() 43 | 44 | call_command('migrate', 'app', interactive=False) 45 | 46 | def destroy_test_db(self, old_database_name=None, verbosity=1, keepdb=False, suffix=None): 47 | self.tarantool_proc.kill() 48 | super().destroy_test_db(old_database_name, verbosity, keepdb, suffix) 49 | 50 | def _destroy_test_db(self, test_database_name, verbosity): 51 | shutil.rmtree(self.db_path) 52 | -------------------------------------------------------------------------------- /django_tarantool/backend/features.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.base.features import BaseDatabaseFeatures 2 | from django.utils.functional import cached_property 3 | 4 | 5 | class DatabaseFeatures(BaseDatabaseFeatures): 6 | uses_savepoints = False 7 | can_alter_table_rename_column = False 8 | can_introspect_autofield = True # 9 | can_introspect_big_integer_field = False 10 | can_introspect_binary_field = False # 11 | can_introspect_decimal_field = False # 12 | can_introspect_duration_field = False # 13 | can_introspect_ip_address_field = False # 14 | can_introspect_time_field = False # 15 | 16 | max_query_params = 65000 17 | 18 | @cached_property 19 | def supports_transactions(self): 20 | return False 21 | -------------------------------------------------------------------------------- /django_tarantool/backend/introspection.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from django.db.backends.base.introspection import BaseDatabaseIntrospection, \ 4 | TableInfo, FieldInfo as BaseFieldInfo 5 | 6 | 7 | FieldInfo = namedtuple('FieldInfo', BaseFieldInfo._fields + ('pk',)) 8 | 9 | 10 | class DatabaseIntrospection(BaseDatabaseIntrospection): 11 | data_types_reverse = { 12 | 'bool': 'BooleanField', 13 | 'boolean': 'BooleanField', 14 | 'integer': 'IntegerField', 15 | 'number': 'FloatField', 16 | 'unsigned': 'PositiveIntegerField', 17 | 'decimal': 'DecimalField', 18 | 'text': 'TextField', 19 | 'string': 'CharField', 20 | 'varchar': 'CharField', 21 | } 22 | 23 | def get_field_type(self, data_type, description): 24 | field_type = super().get_field_type(data_type, description) 25 | if description.pk and field_type in {'BigIntegerField', 'IntegerField', 26 | 'SmallIntegerField'}: 27 | return 'AutoField' 28 | return field_type 29 | 30 | def get_constraints(self, cursor, table_name): 31 | """ 32 | Retrieve any constraints or keys (unique, pk, fk, check, index) 33 | across one or more columns. 34 | 35 | Return a dict mapping constraint names to their attributes, 36 | where attributes is a dict with keys: 37 | * columns: List of columns this covers 38 | * primary_key: True if primary key, False otherwise 39 | * unique: True if this is a unique constraint, False otherwise 40 | * foreign_key: (table, column) of target, or None 41 | * check: True if check constraint, False otherwise 42 | * index: True if index, False otherwise. 43 | * orders: The order (ASC/DESC) defined for the columns of indexes 44 | * type: The type of the index (btree, hash, etc.) 45 | """ 46 | 47 | constraints = {} 48 | cursor.execute(""" 49 | SELECT 50 | "name" AS constraint_name, 51 | (SELECT "name" FROM "_vspace" x WHERE x."id" = y."id") AS 52 | table_name, 53 | CASE WHEN "iid" = 0 THEN 'PRIMARY' ELSE 'UNIQUE' END AS 54 | constraint_type, 55 | "id" AS id, 56 | "iid" AS iid, 57 | "type" AS index_type 58 | FROM "_vindex" y 59 | WHERE table_name = %s;""", [table_name]) 60 | 61 | for index, table_name, constraint_type, _id, iid, index_type \ 62 | in cursor.fetchall(): 63 | 64 | constraints[index] = { 65 | "columns": [], 66 | "primary_key": constraint_type == 'PRIMARY', 67 | "unique": constraint_type == 'UNIQUE', 68 | "foreign_key": None, 69 | "check": False, 70 | "index": False, 71 | } 72 | cursor.execute('PRAGMA index_info(%s.%s)' % ( 73 | self.connection.ops.quote_name(table_name), 74 | self.connection.ops.quote_name(index) 75 | )) 76 | 77 | for constraint_data in cursor.fetchall(): 78 | constraints[index]['columns'].append(constraint_data[2]) 79 | 80 | cursor.execute( 81 | 'PRAGMA foreign_key_list(%s)' % self.connection.ops.quote_name( 82 | table_name)) 83 | for row in cursor.fetchall(): 84 | # Remaining on_update/on_delete/match values are of no interest. 85 | id_, _, table, from_, to = row[:5] 86 | constraints['fk_%d' % id_] = { 87 | 'columns': [from_], 88 | 'primary_key': False, 89 | 'unique': False, 90 | 'foreign_key': (table, to), 91 | 'check': False, 92 | 'index': False, 93 | } 94 | cursor.execute(""" 95 | SELECT 96 | "name" AS constraint_name, 97 | "code" AS check_clause, 98 | (SELECT "name" FROM "_vspace" x WHERE x."id" = y."space_id") AS 99 | table_name, 100 | "language" AS language, 101 | "is_deferred" AS is_deferred, 102 | "space_id" AS space_id 103 | FROM "_ck_constraint" y 104 | WHERE table_name = %s;""", [table_name]) 105 | for row in cursor.fetchall(): 106 | constraint_name = row[0] 107 | constraints[constraint_name] = { 108 | 'columns': [], 109 | 'primary_key': False, 110 | 'unique': False, 111 | 'foreign_key': None, 112 | 'check': True, 113 | 'index': False, 114 | } 115 | return constraints 116 | 117 | def get_relations(self, cursor, table_name): 118 | """ 119 | Return a dictionary of {field_name: (field_name_other_table, 120 | other_table)} representing all relationships to the given table. 121 | """ 122 | # Dictionary of relations to return 123 | relations = { 124 | key[0]: (key[2], key[1]) 125 | for key in self.get_key_columns(cursor, table_name) 126 | } 127 | 128 | return relations 129 | 130 | def get_table_description(self, cursor, table_name): 131 | """ 132 | Return a description of the table with the DB-API cursor.description 133 | interface. 134 | """ 135 | cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name( 136 | table_name)) 137 | return [ 138 | FieldInfo( 139 | name, data_type, None, None, None, None, 140 | not notnull, default, pk == 1, 141 | ) 142 | for cid, name, data_type, notnull, default, pk in cursor.fetchall() 143 | ] 144 | 145 | def get_key_columns(self, cursor, table_name): 146 | """ 147 | Return a list of (column_name, referenced_table_name, 148 | referenced_column_name) 149 | for all key columns in given table. 150 | """ 151 | cursor.execute( 152 | 'PRAGMA foreign_key_list(%s)' % self.connection.ops.quote_name( 153 | table_name)) 154 | key_columns = [] 155 | for _, _, referenced_table_name, column_name, referenced_column_name,\ 156 | _, _, _ in cursor.fetchall(): 157 | key_columns.append( 158 | [column_name, referenced_table_name, referenced_column_name]) 159 | return key_columns 160 | 161 | def get_sequences(self, cursor, table_name, table_fields=()): 162 | """ 163 | Return a list of introspected sequences for table_name. Each sequence 164 | is a dict: {'table': , 'column': }. An optional 165 | 'name' key can be added if the backend supports named sequences. 166 | """ 167 | pk_col = self.get_primary_key_column(cursor, table_name) 168 | return [{'table': table_name, 'column': pk_col}] 169 | 170 | def get_table_list(self, cursor): 171 | cursor.execute('SELECT "name" FROM "_space" ' 172 | 'WHERE "name" NOT LIKE \'X_%\' ESCAPE \'X\'') 173 | 174 | return [TableInfo(table[0], 't') for table in cursor.fetchall()] 175 | -------------------------------------------------------------------------------- /django_tarantool/backend/operations.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.conf import settings 4 | from django.utils import timezone 5 | from django.utils.datetime_safe import datetime 6 | from django.db.backends.base.operations import BaseDatabaseOperations 7 | from datetime import time 8 | 9 | 10 | class DatabaseOperations(BaseDatabaseOperations): 11 | 12 | def quote_name(self, name): 13 | return '"%s"' % name 14 | 15 | def pk_default_value(self): 16 | return 'NULL' 17 | 18 | def get_db_converters(self, expression): 19 | converters = [] 20 | internal_type = expression.output_field.get_internal_type() 21 | if internal_type == 'DateTimeField': 22 | converters.append(self.convert_datetimefield_value) 23 | elif internal_type == 'DateField': 24 | converters.append(self.convert_datefield_value) 25 | elif internal_type == 'TimeField': 26 | converters.append(self.convert_timefield_value) 27 | elif internal_type == 'UUIDField': 28 | converters.append(self.convert_uuidfield_value) 29 | elif internal_type in ('BooleanField', 'NullBooleanField'): 30 | converters.append(self.convert_booleanfield_value) 31 | return converters 32 | 33 | def convert_booleanfield_value(self, value, expression, connection, *args, 34 | **kwargs): 35 | return value 36 | 37 | def convert_datefield_value(self, value, expression, connection, *args, 38 | **kwargs): 39 | if value is None: 40 | return None 41 | return datetime.fromtimestamp(int(value)).date() 42 | 43 | def convert_datetimefield_value(self, value, expression, connection, *args, 44 | **kwargs): 45 | if value is None: 46 | return None 47 | value = datetime.fromtimestamp(value) 48 | if settings.USE_TZ: 49 | value = timezone.make_aware(value, self.connection.timezone) 50 | return value 51 | 52 | def convert_timefield_value(self, value, expression, connection, *args, 53 | **kwargs): 54 | if value is None: 55 | return None 56 | hours, _seconds = divmod(value, 3600) 57 | minutes, seconds = divmod(_seconds, 60) 58 | return time(hours, minutes, seconds) 59 | 60 | def convert_uuidfield_value(self, value, expression, connection, *args, 61 | **kwargs): 62 | if value is not None: 63 | value = uuid.UUID(value) 64 | return value 65 | 66 | def adapt_datetimefield_value(self, value): 67 | return value.timestamp() if value else None 68 | 69 | def adapt_datefield_value(self, value): 70 | return datetime.fromordinal(value.toordinal()).timestamp() if value is not None else None 71 | 72 | def adapt_timefield_value(self, value): 73 | return value.hour * 60 * 60 + value.minute * 60 + value.second if \ 74 | value is not None else None 75 | 76 | def lookup_cast(self, lookup_type, internal_type=None): 77 | if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): 78 | return 'UPPER(%s)' 79 | return '%s' 80 | 81 | def bulk_insert_sql(self, fields, placeholder_rows): 82 | placeholder_rows_sql = (", ".join(row) for row in placeholder_rows) 83 | values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql) 84 | return "VALUES " + values_sql 85 | 86 | def bulk_batch_size(self, fields, objs): 87 | if fields: 88 | return self.connection.features.max_query_params // len(fields) 89 | return len(objs) 90 | 91 | def no_limit_value(self): 92 | return 131072 93 | 94 | def sql_flush(self, style, tables, *args, **kwargs): 95 | return [] 96 | -------------------------------------------------------------------------------- /django_tarantool/backend/schema.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from random import choice 3 | from string import ascii_lowercase 4 | 5 | from django.apps.registry import Apps 6 | from django.db.backends.base.schema import BaseDatabaseSchemaEditor 7 | from .utils import strip_quotes 8 | 9 | try: 10 | from django.db.backends.ddl_references import Statement 11 | 12 | use_ddl_references_statement = True 13 | except ImportError: 14 | use_ddl_references_statement = False 15 | 16 | try: 17 | from django.db.models import UniqueConstraint 18 | 19 | use_unique_constraints = True 20 | except ImportError: 21 | use_unique_constraints = False 22 | 23 | 24 | class AbstractDatabaseSchemaEditor(BaseDatabaseSchemaEditor): 25 | sql_delete_table = "DROP TABLE %(table)s" 26 | sql_create_inline_fk = "REFERENCES %(to_table)s (%(to_column)s) " \ 27 | "DEFERRABLE INITIALLY DEFERRED" 28 | sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(" \ 29 | "columns)s)" 30 | sql_delete_unique = 'DROP INDEX %(name)s ON %(table)s' 31 | 32 | def quote_value(self, value): 33 | if value is None: 34 | return "NULL" 35 | if isinstance(value, bool): 36 | return str(value) 37 | return "'%s'" % value 38 | 39 | def _is_referenced_by_fk_constraint(self, table_name, column_name=None, ignore_self=False): 40 | """ 41 | Return whether or not the provided table name is referenced by another 42 | one. If `column_name` is specified, only references pointing to that 43 | column are considered. If `ignore_self` is True, self-referential 44 | constraints are ignored. 45 | """ 46 | with self.connection.cursor() as cursor: 47 | for other_table in self.connection.introspection.get_table_list(cursor): 48 | if ignore_self and other_table.name == table_name: 49 | continue 50 | constraints = self.connection.introspection._get_foreign_key_constraints(cursor, other_table.name) 51 | for constraint in constraints.values(): 52 | constraint_table, constraint_column = constraint['foreign_key'] 53 | if (constraint_table == table_name and 54 | (column_name is None or constraint_column == column_name)): 55 | return True 56 | return False 57 | 58 | def _remake_table(self, model, create_field=None, delete_field=None, alter_field=None): 59 | """ 60 | Shortcut to transform a model from old_model into new_model 61 | 62 | The essential steps are: 63 | 1. Create a table with the updated definition called "new__(rand_name)__app_model" 64 | 2. Copy the data from the existing "app_model" table to the new table 65 | 3. If there is foreign key that references the table, drop the referencing constraints 66 | 4. Drop the "app_model" table 67 | 5. Rename the "new__(rand_name)__app_model" table to "app_model" 68 | 6. Restore any index of the previous "app_model" table. 69 | """ 70 | # Self-referential fields must be recreated rather than copied from 71 | # the old model to ensure their remote_field.field_name doesn't refer 72 | # to an altered field. 73 | def is_self_referential(f): 74 | return f.is_relation and f.remote_field.model is model 75 | # Work out the new fields dict / mapping 76 | body = { 77 | f.name: f.clone() if is_self_referential(f) else f 78 | for f in model._meta.local_concrete_fields 79 | } 80 | # Since mapping might mix column names and default values, 81 | # its values must be already quoted. 82 | mapping = {f.column: self.quote_name(f.column) for f in model._meta.local_concrete_fields} 83 | # This maps field names (not columns) for things like unique_together 84 | rename_mapping = {} 85 | # If any of the new or altered fields is introducing a new PK, 86 | # remove the old one 87 | restore_pk_field = None 88 | if getattr(create_field, 'primary_key', False) or ( 89 | alter_field and getattr(alter_field[1], 'primary_key', False)): 90 | for name, field in list(body.items()): 91 | if field.primary_key: 92 | field.primary_key = False 93 | restore_pk_field = field 94 | if field.auto_created: 95 | del body[name] 96 | del mapping[field.column] 97 | # Add in any created fields 98 | if create_field: 99 | body[create_field.name] = create_field 100 | # Choose a default and insert it into the copy map 101 | if not create_field.many_to_many and create_field.concrete: 102 | mapping[create_field.column] = self.quote_value( 103 | self.effective_default(create_field) 104 | ) 105 | # Add in any altered fields 106 | if alter_field: 107 | old_field, new_field = alter_field 108 | body.pop(old_field.name, None) 109 | mapping.pop(old_field.column, None) 110 | body[new_field.name] = new_field 111 | if old_field.null and not new_field.null: 112 | case_sql = "coalesce(%(col)s, %(default)s)" % { 113 | 'col': self.quote_name(old_field.column), 114 | 'default': self.quote_value(self.effective_default(new_field)) 115 | } 116 | mapping[new_field.column] = case_sql 117 | else: 118 | mapping[new_field.column] = self.quote_name(old_field.column) 119 | rename_mapping[old_field.name] = new_field.name 120 | # Remove any deleted fields 121 | if delete_field: 122 | del body[delete_field.name] 123 | del mapping[delete_field.column] 124 | # Remove any implicit M2M tables 125 | if delete_field.many_to_many and delete_field.remote_field.through._meta.auto_created: 126 | return self.delete_model(delete_field.remote_field.through) 127 | # Work inside a new app registry 128 | apps = Apps() 129 | 130 | # Work out the new value of unique_together, taking renames into 131 | # account 132 | unique_together = [ 133 | [rename_mapping.get(n, n) for n in unique] 134 | for unique in model._meta.unique_together 135 | ] 136 | 137 | # Work out the new value for index_together, taking renames into 138 | # account 139 | index_together = [ 140 | [rename_mapping.get(n, n) for n in index] 141 | for index in model._meta.index_together 142 | ] 143 | 144 | indexes = model._meta.indexes 145 | if delete_field: 146 | indexes = [ 147 | index for index in indexes 148 | if delete_field.name not in index.fields 149 | ] 150 | 151 | if use_unique_constraints: 152 | constraints = list(model._meta.constraints) 153 | 154 | # Provide isolated instances of the fields to the new model body so 155 | # that the existing model's internals aren't interfered with when 156 | # the dummy model is constructed. 157 | body_copy = copy.deepcopy(body) 158 | 159 | # Construct a new model with the new fields to allow self referential 160 | # primary key to resolve to. This model won't ever be materialized as a 161 | # table and solely exists for foreign key reference resolution purposes. 162 | # This wouldn't be required if the schema editor was operating on model 163 | # states instead of rendered models. 164 | meta_contents = { 165 | 'app_label': model._meta.app_label, 166 | 'db_table': model._meta.db_table, 167 | 'unique_together': unique_together, 168 | 'index_together': index_together, 169 | 'indexes': indexes, 170 | 'apps': apps, 171 | } 172 | if use_unique_constraints: 173 | meta_contents['constraints'] = constraints 174 | 175 | meta = type("Meta", (), meta_contents) 176 | body_copy['Meta'] = meta 177 | body_copy['__module__'] = model.__module__ 178 | type(model._meta.object_name, model.__bases__, body_copy) 179 | 180 | # Construct a model with a renamed table name. 181 | body_copy = copy.deepcopy(body) 182 | suffix = ''.join(choice(ascii_lowercase) for _ in range(6)) 183 | meta_contents = { 184 | 'app_label': model._meta.app_label, 185 | 'db_table': 'new__%s__%s' % ( 186 | suffix, strip_quotes(model._meta.db_table)), 187 | 'unique_together': unique_together, 188 | 'index_together': index_together, 189 | 'indexes': indexes, 190 | 'apps': apps, 191 | } 192 | if use_unique_constraints: 193 | meta_contents['constraints'] = constraints 194 | 195 | meta = type("Meta", (), meta_contents) 196 | body_copy['Meta'] = meta 197 | body_copy['__module__'] = model.__module__ 198 | new_model = type('New%s' % model._meta.object_name, model.__bases__, 199 | body_copy) 200 | 201 | # Create a new table with the updated schema. 202 | self.create_model(new_model) 203 | 204 | # Copy data from the old table into the new table 205 | self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % ( 206 | self.quote_name(new_model._meta.db_table), 207 | ', '.join(self.quote_name(x) for x in mapping), 208 | ', '.join(mapping.values()), 209 | self.quote_name(model._meta.db_table), 210 | )) 211 | 212 | # Delete the old table to make way for the new 213 | self.delete_model(model, handle_autom2m=False) 214 | 215 | # Rename the new table to take way for the old 216 | self.alter_db_table( 217 | new_model, new_model._meta.db_table, model._meta.db_table, 218 | # disable_constraints=False, 219 | ) 220 | 221 | # Run deferred SQL on correct table 222 | if use_ddl_references_statement: 223 | for sql in self.deferred_sql: 224 | self.execute(sql) 225 | self.deferred_sql = [] 226 | # Fix any PK-removed field 227 | if restore_pk_field: 228 | restore_pk_field.primary_key = True 229 | 230 | def delete_model(self, model, handle_autom2m=True): 231 | self.drop_referenced_fk(model) 232 | 233 | if handle_autom2m: 234 | super().delete_model(model) 235 | else: 236 | # Delete the table (and only that) 237 | self.execute(self.sql_delete_table % { 238 | "table": self.quote_name(model._meta.db_table), 239 | }) 240 | # Remove all deferred statements referencing the deleted table. 241 | if use_ddl_references_statement: 242 | for sql in list(self.deferred_sql): 243 | if isinstance(sql, Statement) and \ 244 | sql.references_table(model._meta.db_table): 245 | self.deferred_sql.remove(sql) 246 | 247 | def add_field(self, model, field): 248 | """ 249 | Create a field on a model. Usually involves adding a column, but may 250 | involve adding a table instead (for M2M fields). 251 | """ 252 | # Special-case implicit M2M tables 253 | if field.many_to_many and field.remote_field.through._meta.auto_created: 254 | return self.create_model(field.remote_field.through) 255 | self._remake_table(model, create_field=field) 256 | 257 | def remove_field(self, model, field): 258 | """ 259 | Remove a field from a model. Usually involves deleting a column, 260 | but for M2Ms may involve deleting a table. 261 | """ 262 | # M2M fields are a special case 263 | if field.many_to_many: 264 | # For implicit M2M tables, delete the auto-created table 265 | if field.remote_field.through._meta.auto_created: 266 | self.delete_model(field.remote_field.through) 267 | # For explicit "through" M2M fields, do nothing 268 | # For everything else, remake. 269 | else: 270 | # It might not actually have a column behind it 271 | if field.db_parameters(connection=self.connection)['type'] is None: 272 | return 273 | self._remake_table(model, delete_field=field) 274 | 275 | def _alter_field(self, model, old_field, new_field, old_type, new_type, 276 | old_db_params, new_db_params, strict=False): 277 | """Perform a "physical" (non-ManyToMany) field update.""" 278 | # Use "ALTER TABLE ... RENAME COLUMN" if only the column name 279 | # changed and there aren't any constraints. 280 | if (self.connection.features.can_alter_table_rename_column and 281 | old_field.column != new_field.column and 282 | self.column_sql(model, old_field) == self.column_sql(model, new_field) and 283 | not (old_field.remote_field and old_field.db_constraint or 284 | new_field.remote_field and new_field.db_constraint)): 285 | return self.execute(self._rename_field_sql(model._meta.db_table, old_field, new_field, new_type)) 286 | # Alter by remaking table 287 | self._remake_table(model, alter_field=(old_field, new_field)) 288 | # Rebuild tables with FKs pointing to this field if the PK type changed. 289 | if old_field.primary_key and new_field.primary_key and old_type != new_type: 290 | for rel in new_field.model._meta.related_objects: 291 | if not rel.many_to_many: 292 | self._remake_table(rel.related_model) 293 | 294 | def _alter_many_to_many(self, model, old_field, new_field, strict): 295 | """Alter M2Ms to repoint their to= endpoints.""" 296 | if old_field.remote_field.through._meta.db_table == new_field.remote_field.through._meta.db_table: 297 | # The field name didn't change, but some options did; we have to propagate this altering. 298 | self._remake_table( 299 | old_field.remote_field.through, 300 | alter_field=( 301 | # We need the field that points to the target model, so we can tell alter_field to change it - 302 | # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model) 303 | old_field.remote_field.through._meta.get_field(old_field.m2m_reverse_field_name()), 304 | new_field.remote_field.through._meta.get_field(new_field.m2m_reverse_field_name()), 305 | ), 306 | ) 307 | return 308 | 309 | # Make a new through table 310 | self.create_model(new_field.remote_field.through) 311 | # Copy the data across 312 | self.execute("INSERT INTO %s (%s) SELECT %s FROM %s" % ( 313 | self.quote_name(new_field.remote_field.through._meta.db_table), 314 | ', '.join([ 315 | "id", 316 | new_field.m2m_column_name(), 317 | new_field.m2m_reverse_name(), 318 | ]), 319 | ', '.join([ 320 | "id", 321 | old_field.m2m_column_name(), 322 | old_field.m2m_reverse_name(), 323 | ]), 324 | self.quote_name(old_field.remote_field.through._meta.db_table), 325 | )) 326 | # Delete the old through table 327 | self.delete_model(old_field.remote_field.through) 328 | 329 | def drop_referenced_fk(self, model): 330 | sql = """ 331 | SELECT constraint_name, referencing FROM 332 | (SELECT "name" AS constraint_name, 333 | (SELECT "name" 334 | FROM "_vspace" x 335 | WHERE x."id" = y."child_id") 336 | AS referencing, 337 | (SELECT "name" 338 | FROM "_vspace" x 339 | WHERE x."id" = y."parent_id") 340 | AS referenced 341 | FROM "_fk_constraint" y where referenced = '%s') 342 | """ % model._meta.db_table 343 | with self.connection.cursor() as cursor: 344 | cursor.execute(sql) 345 | for constraint, table in cursor.rows: 346 | cursor.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % ( 347 | table, constraint)) 348 | 349 | 350 | if use_unique_constraints: 351 | class DatabaseSchemaEditor(AbstractDatabaseSchemaEditor): 352 | pass 353 | else: 354 | class DatabaseSchemaEditor(AbstractDatabaseSchemaEditor): 355 | def add_constraint(self, model, constraint): 356 | if isinstance(constraint, 357 | UniqueConstraint) and constraint.condition: 358 | super().add_constraint(model, constraint) 359 | else: 360 | self._remake_table(model) 361 | 362 | def remove_constraint(self, model, constraint): 363 | if isinstance(constraint, 364 | UniqueConstraint) and constraint.condition: 365 | super().remove_constraint(model, constraint) 366 | else: 367 | self._remake_table(model) 368 | -------------------------------------------------------------------------------- /django_tarantool/backend/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def wait_for_tarantool(connection): 5 | while True: 6 | try: 7 | cursor = connection.cursor() 8 | cursor.execute("SELECT 1") 9 | except Exception as e: 10 | time.sleep(.5) 11 | continue 12 | finally: 13 | break 14 | 15 | 16 | def strip_quotes(table_name): 17 | """ 18 | Strip quotes off of quoted table names to make them safe for use in index 19 | names, sequence names, etc. For example '"USER"."TABLE"' (an Oracle naming 20 | scheme) becomes 'USER"."TABLE'. 21 | """ 22 | has_quotes = table_name.startswith('"') and table_name.endswith('"') 23 | return table_name[1:-1] if has_quotes else table_name 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=3.0.0,<3.1 2 | tarantool>=0.7.1 3 | msgpack==1.0.2 4 | model-bakery==1.1.0 5 | psycopg2-binary==2.8.4 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="django-tarantool", 10 | version="0.0.20", 11 | package_dir={"django-tarantool": os.path.join("django_tarantool")}, 12 | author="Artem Morozov", 13 | author_email="artembo@me.com", 14 | description="Tarantool database backend for Django", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/artembo/django-tarantool", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | 'Environment :: Web Environment', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 3 :: Only', 23 | 'Framework :: Django', 24 | 'Framework :: Django :: 1.10', 25 | 'Framework :: Django :: 1.11', 26 | 'Framework :: Django :: 2.0', 27 | 'Framework :: Django :: 2.1', 28 | 'Framework :: Django :: 2.2', 29 | 'Framework :: Django :: 3.0', 30 | 'Framework :: Django :: 3.1', 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | ], 34 | python_requires='>=3.6', 35 | install_requires=[ 36 | 'tarantool>=0.7.1', 37 | ], 38 | ) 39 | --------------------------------------------------------------------------------