├── .github └── workflows │ ├── linters.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── django-test-suite ├── build.sh ├── cockroach_gis_settings.py ├── cockroach_settings.py └── runtests.py ├── django_cockroachdb ├── __init__.py ├── base.py ├── client.py ├── creation.py ├── features.py ├── functions.py ├── introspection.py ├── lookups.py ├── operations.py ├── schema.py └── utils.py ├── django_cockroachdb_gis ├── __init__.py ├── base.py ├── features.py ├── functions.py ├── introspection.py ├── operations.py └── schema.py ├── setup.cfg └── setup.py /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | flake8: 12 | name: flake8 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | - run: python -m pip install flake8 22 | - name: flake8 23 | uses: liskin/gh-problem-matcher-wrap@v2 24 | with: 25 | linters: flake8 26 | run: flake8 27 | 28 | isort: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Python 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: '3.10' 37 | - run: python -m pip install isort 38 | - name: isort 39 | uses: liskin/gh-problem-matcher-wrap@v2 40 | with: 41 | linters: isort 42 | run: isort --check --diff . 43 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Django test suite 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | # This job aggregates all matrix results and is used for a GitHub required status check. 12 | test_results: 13 | if: ${{ always() }} 14 | runs-on: ubuntu-24.04 15 | name: Test Results 16 | needs: [test] 17 | steps: 18 | - run: | 19 | result="${{ needs.test.result }}" 20 | if [[ $result == "success" || $result == "skipped" ]]; then 21 | exit 0 22 | else 23 | exit 1 24 | fi 25 | 26 | test: 27 | runs-on: ubuntu-24.04 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | include: 32 | - crdb-version: v23.2.21 33 | - crdb-version: v23.2.21 34 | use_psycopg2: psycopg2 35 | - crdb-version: v23.2.21 36 | use_server_side_binding: server_side_binding 37 | - crdb-version: v24.1.14 38 | - crdb-version: v24.1.14 39 | use_psycopg2: psycopg2 40 | - crdb-version: v24.1.14 41 | use_server_side_binding: server_side_binding 42 | - crdb-version: v24.3.8 43 | - crdb-version: v24.3.8 44 | use_psycopg2: psycopg2 45 | - crdb-version: v24.3.8 46 | use_server_side_binding: server_side_binding 47 | - crdb-version: v25.1.2 48 | - crdb-version: v25.1.2 49 | use_psycopg2: psycopg2 50 | - crdb-version: v25.1.2 51 | use_server_side_binding: server_side_binding 52 | # Uncomment to enable testing of CockroachDB nightly. 53 | #- crdb-version: LATEST 54 | #- crdb-version: LATEST 55 | # use_psycopg2: psycopg2 56 | #- crdb-version: LATEST 57 | # use_server_side_binding: server_side_binding 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: actions/setup-python@v4 61 | with: 62 | python-version: '3.10' 63 | - name: Install system packages for Django's Python test dependencies 64 | run: | 65 | sudo apt-get update 66 | sudo apt-get install binutils gdal-bin 67 | - name: Run build.sh 68 | run: bash django-test-suite/build.sh ${{ matrix.crdb-version }} 69 | env: 70 | USE_PSYCOPG2: ${{ matrix.use_psycopg2 }} 71 | USE_SERVER_SIDE_BINDING: ${{ matrix.use_server_side_binding }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.2 - 2025-04-07 4 | 5 | Initial release for Django 5.2.x and CockroachDB 23.2.x, 24.1.x, 24.3.x, and 6 | 25.1.x. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CockroachDB backend for Django 2 | 3 | ## Prerequisites 4 | 5 | You must install: 6 | 7 | * [psycopg](https://pypi.org/project/psycopg/), which may have some 8 | prerequisites [depending on which version you use](https://www.psycopg.org/psycopg3/docs/basic/install.html). 9 | 10 | You can also use either: 11 | 12 | * [psycopg2](https://pypi.org/project/psycopg2/), which has some 13 | [prerequisites](https://www.psycopg.org/docs/install.html#prerequisites) of 14 | its own. 15 | 16 | * [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) 17 | 18 | The binary package is a practical choice for development and testing but in 19 | production it is advised to use the package built from sources. 20 | 21 | ## Install and usage 22 | 23 | Use the version of django-cockroachdb that corresponds to your version of 24 | Django. For example, to get the latest compatible release for Django 5.2.x: 25 | 26 | `pip install django-cockroachdb==5.2.*` 27 | 28 | The minor release number of Django doesn't correspond to the minor release 29 | number of django-cockroachdb. Use the latest minor release of each. 30 | 31 | Configure the Django `DATABASES` setting similar to this: 32 | 33 | ```python 34 | DATABASES = { 35 | 'default': { 36 | 'ENGINE': 'django_cockroachdb', 37 | 'NAME': 'django', 38 | 'USER': 'myprojectuser', 39 | 'PASSWORD': '', 40 | 'HOST': 'localhost', 41 | 'PORT': '26257', 42 | # If connecting with SSL, include the section below, replacing the 43 | # file paths as appropriate. 44 | 'OPTIONS': { 45 | 'sslmode': 'verify-full', 46 | 'sslrootcert': '/certs/ca.crt', 47 | # Either sslcert and sslkey (below) or PASSWORD (above) is 48 | # required. 49 | 'sslcert': '/certs/client.myprojectuser.crt', 50 | 'sslkey': '/certs/client.myprojectuser.key', 51 | # If applicable 52 | 'options': '--cluster={routing-id}', 53 | }, 54 | }, 55 | } 56 | ``` 57 | 58 | If using Kerberos authentication, you can specify a custom service name in 59 | `'OPTIONS'` using the key `'krbsrvname'`. 60 | 61 | ## Notes on Django fields 62 | 63 | - `IntegerField` uses the same storage as `BigIntegerField` so `IntegerField` 64 | is introspected by `inspectdb` as `BigIntegerField`. 65 | 66 | - `AutoField` and `BigAutoField` are both stored as 67 | [integer](https://www.cockroachlabs.com/docs/stable/int.html) (64-bit) with 68 | [`DEFAULT unique_rowid()`](https://www.cockroachlabs.com/docs/stable/functions-and-operators.html#id-generation-functions). 69 | 70 | ## Notes on Django QuerySets 71 | 72 | - [`QuerySet.explain()`](https://docs.djangoproject.com/en/stable/ref/models/querysets/#explain) 73 | accepts `verbose`, `types`, `opt`, `vec`, and `distsql` options which 74 | correspond to [CockroachDB's parameters](https://www.cockroachlabs.com/docs/stable/explain.html#parameters). 75 | For example: 76 | 77 | ```python 78 | >>> Choice.objects.explain(opt=True, verbose=True) 79 | 'scan polls_choice\n ├── columns: id:1 question_id:4 choice_text:2 votes:3\n ├── stats: [rows=1]\n ├── cost: 1.1\n ├── key: (1)\n ├── fd: (1)-->(2-4)\n └── prune: (1-4)' 80 | ``` 81 | 82 | ## FAQ 83 | 84 | ## GIS support 85 | 86 | To use `django.contrib.gis` with CockroachDB, use 87 | `'ENGINE': 'django_cockroachdb_gis'` in Django's `DATABASES` setting. 88 | 89 | ## Disabling CockroachDB telemetry 90 | 91 | By default, CockroachDB sends the version of django-cockroachdb that you're 92 | using back to Cockroach Labs. To disable this, set 93 | `DISABLE_COCKROACHDB_TELEMETRY = True` in your Django settings. 94 | 95 | ## Known issues and limitations in CockroachDB 25.1.x and earlier 96 | 97 | - CockroachDB [can't disable constraint checking](https://github.com/cockroachdb/cockroach/issues/19444), 98 | which means certain things in Django like forward references in fixtures 99 | aren't supported. 100 | 101 | - Migrations have some limitations. CockroachDB doesn't support: 102 | 103 | - [changing column type if it's part of an index](https://go.crdb.dev/issue/47636) 104 | - dropping or changing a table's primary key 105 | 106 | - The `Field.db_comment` and `Meta.db_table_comment` options aren't supported 107 | due to [poor performance](https://github.com/cockroachdb/cockroach/issues/95068). 108 | 109 | - Unsupported queries: 110 | - [Mixed type addition in SELECT](https://github.com/cockroachdb/django-cockroachdb/issues/19): 111 | `unsupported binary operator: + ` 112 | - [Division that yields a different type](https://github.com/cockroachdb/django-cockroachdb/issues/21): 113 | `unsupported binary operator: / (desired )` 114 | - [The power() database function doesn't accept negative exponents](https://github.com/cockroachdb/django-cockroachdb/issues/22): 115 | `power(): integer out of range` 116 | - [sum() doesn't support arguments of different types](https://github.com/cockroachdb/django-cockroachdb/issues/73): 117 | `sum(): unsupported binary operator: + ` 118 | - [greatest() doesn't support arguments of different types](https://github.com/cockroachdb/django-cockroachdb/issues/74): 119 | `greatest(): expected to be of type , found type ` 120 | - [`SmallAutoField` generates values that are too large for any corresponding foreign keys](https://github.com/cockroachdb/django-cockroachdb/issues/84). 121 | 122 | - GIS: 123 | - Some database functions aren't supported: `AsGML`, `AsKML`, `AsSVG`, 124 | and `GeometryDistance`. 125 | - Some 3D functions or signatures aren't supported: `ST_3DPerimeter`, 126 | `ST_3DExtent`, `ST_Scale`, and `ST_LengthSpheroid`. 127 | - The `Length` database function isn't supported on geodetic fields: 128 | [st_lengthspheroid(): unimplemented](https://github.com/cockroachdb/cockroach/issues/48968). 129 | - `Union` may crash with 130 | [unknown signature: st_union(geometry, geometry)](https://github.com/cockroachdb/cockroach/issues/49064). 131 | - The spheroid argument of ST_DistanceSpheroid 132 | [isn't supported](https://github.com/cockroachdb/cockroach/issues/48922): 133 | `unknown signature: st_distancespheroid(geometry, geometry, string)`. 134 | - These lookups aren't supported: 135 | - [contained (@)](https://github.com/cockroachdb/cockroach/issues/56124) 136 | - [exact/same_as (~=)](https://github.com/cockroachdb/cockroach/issues/57096) 137 | - [left (<<) and right (>>)](https://github.com/cockroachdb/cockroach/issues/57092) 138 | - [overlaps_left (&<), overlaps_right (&>), overlaps_above (&<|), 139 | overlaps_below (&>|)](https://github.com/cockroachdb/cockroach/issues/57098) 140 | - [strictly_above (|>>), strictly_below (<<|)](https://github.com/cockroachdb/cockroach/issues/57095) 141 | 142 | ## Known issues and limitations in CockroachDB 24.3.x and earlier 143 | 144 | - CockroachDB executes `ALTER COLUMN` queries asynchronously which is at 145 | odds with Django's assumption that the database is altered before the next 146 | migration operation begins. CockroachDB will give an error like 147 | `unimplemented: table <...> is currently undergoing a schema change` if a 148 | later operation tries to modify the table before the asynchronous query 149 | finishes. 150 | -------------------------------------------------------------------------------- /django-test-suite/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | # This script expects the first argument to be a CockroachDB version like 5 | # "v22.1.18" or "LATEST" to test with the nightly CockroachDB build. 6 | VERSION=$1 7 | 8 | # clone django into the repo. 9 | rm -rf _django_repo 10 | git clone --depth 1 --single-branch --branch cockroach-5.2.x https://github.com/timgraham/django _django_repo 11 | 12 | # install the django requirements. 13 | cd _django_repo/tests/ 14 | pip3 install -e .. 15 | pip3 install -r requirements/py3.txt 16 | if [ "${USE_PSYCOPG2}" == "psycopg2" ]; then 17 | pip3 install psycopg2 18 | else 19 | pip3 install -r requirements/postgres.txt 20 | fi 21 | cd ../.. 22 | 23 | # install the django-cockroachdb backend. 24 | pip3 install . 25 | 26 | # download and start cockroach 27 | if [ $VERSION == "LATEST" ] 28 | then 29 | SPATIAL_LIBS="--spatial-libs=$PWD" 30 | wget "https://edge-binaries.cockroachdb.com/cockroach/cockroach.linux-gnu-amd64.LATEST" -O cockroach_exec 31 | wget "https://edge-binaries.cockroachdb.com/cockroach/lib/libgeos.linux-gnu-amd64.so.LATEST" -O libgeos.so 32 | wget "https://edge-binaries.cockroachdb.com/cockroach/lib/libgeos_c.linux-gnu-amd64.so.LATEST" -O libgeos_c.so 33 | chmod +x cockroach_exec 34 | else 35 | SPATIAL_LIBS="--spatial-libs=cockroach-${VERSION}.linux-amd64/lib" 36 | wget "https://binaries.cockroachdb.com/cockroach-${VERSION}.linux-amd64.tgz" 37 | tar -xvf cockroach-${VERSION}* 38 | cp cockroach-${VERSION}*/cockroach cockroach_exec 39 | fi 40 | 41 | ./cockroach_exec start-single-node --insecure $SPATIAL_LIBS & 42 | 43 | cd _django_repo/tests/ 44 | 45 | # Bring in the settings needed to run the tests with cockroach. 46 | cp ../../django-test-suite/cockroach_settings.py . 47 | cp ../../django-test-suite/cockroach_gis_settings.py . 48 | 49 | # Run the tests! 50 | python3 ../../django-test-suite/runtests.py 51 | -------------------------------------------------------------------------------- /django-test-suite/cockroach_gis_settings.py: -------------------------------------------------------------------------------- 1 | from cockroach_settings import ( 2 | DATABASES, DEFAULT_AUTO_FIELD, PASSWORD_HASHERS, SECRET_KEY, USE_TZ, 3 | ) 4 | 5 | __all__ = [ 6 | 'DATABASES', 'DEFAULT_AUTO_FIELD', 'PASSWORD_HASHERS', 'SECRET_KEY', 7 | 'USE_TZ', 8 | ] 9 | 10 | DATABASES['default']['ENGINE'] = 'django_cockroachdb_gis' 11 | DATABASES['other']['ENGINE'] = 'django_cockroachdb_gis' 12 | -------------------------------------------------------------------------------- /django-test-suite/cockroach_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django_cockroachdb', 6 | 'NAME': 'django_tests', 7 | 'USER': 'root', 8 | 'PASSWORD': '', 9 | 'HOST': 'localhost', 10 | 'PORT': 26257, 11 | 'OPTIONS': {}, 12 | }, 13 | 'other': { 14 | 'ENGINE': 'django_cockroachdb', 15 | 'NAME': 'django_tests2', 16 | 'USER': 'root', 17 | 'PASSWORD': '', 18 | 'HOST': 'localhost', 19 | 'PORT': 26257, 20 | 'OPTIONS': {}, 21 | }, 22 | } 23 | if os.environ.get('USE_SERVER_SIDE_BINDING') == 'server_side_binding': 24 | DATABASES['default']['OPTIONS']['server_side_binding'] = True 25 | DATABASES['other']['OPTIONS']['server_side_binding'] = True 26 | 27 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 28 | SECRET_KEY = 'django_tests_secret_key' 29 | PASSWORD_HASHERS = [ 30 | 'django.contrib.auth.hashers.MD5PasswordHasher', 31 | ] 32 | USE_TZ = False 33 | -------------------------------------------------------------------------------- /django-test-suite/runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # This file assumes it is being run in the django/tests/ repo. 5 | 6 | shouldFail = False 7 | res = os.system("python3 runtests.py --settings cockroach_settings -v 2") 8 | if res != 0: 9 | shouldFail = True 10 | 11 | res = os.system("python3 runtests.py gis_tests --settings cockroach_gis_settings -v 2") 12 | if res != 0: 13 | shouldFail = True 14 | 15 | sys.exit(int(shouldFail)) 16 | -------------------------------------------------------------------------------- /django_cockroachdb/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '5.2' 2 | 3 | # Check Django compatibility before other imports which may fail if the 4 | # wrong version of Django is installed. 5 | from .utils import check_django_compatability 6 | 7 | check_django_compatability() 8 | 9 | from .functions import register_functions # noqa 10 | from .lookups import patch_lookups # noqa 11 | 12 | patch_lookups() 13 | register_functions() 14 | -------------------------------------------------------------------------------- /django_cockroachdb/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from contextlib import contextmanager 4 | 5 | from django.conf import settings 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.utils.functional import cached_property 8 | 9 | try: 10 | try: 11 | import psycopg # noqa 12 | except ImportError: 13 | import psycopg2 # noqa 14 | except ImportError: 15 | raise ImproperlyConfigured("Error loading psycopg or psycopg2 module") 16 | 17 | from django.db.backends.postgresql.base import ( 18 | DatabaseWrapper as PostgresDatabaseWrapper, 19 | ) 20 | 21 | from . import __version__ as django_cockroachdb_version 22 | from .client import DatabaseClient 23 | from .creation import DatabaseCreation 24 | from .features import DatabaseFeatures 25 | from .introspection import DatabaseIntrospection 26 | from .operations import DatabaseOperations 27 | from .schema import DatabaseSchemaEditor 28 | 29 | RAN_TELEMETRY_QUERY = False 30 | 31 | 32 | class DatabaseWrapper(PostgresDatabaseWrapper): 33 | vendor = 'cockroachdb' 34 | display_name = 'CockroachDB' 35 | 36 | # Override some types from the postgresql adapter. 37 | data_types = dict( 38 | PostgresDatabaseWrapper.data_types, 39 | BigAutoField='integer', 40 | AutoField='integer', 41 | DateTimeField='timestamptz', 42 | ) 43 | data_types_suffix = dict( 44 | PostgresDatabaseWrapper.data_types_suffix, 45 | BigAutoField='DEFAULT unique_rowid()', 46 | # Unsupported: https://github.com/cockroachdb/django-cockroachdb/issues/84 47 | SmallAutoField='', 48 | AutoField='DEFAULT unique_rowid()', 49 | ) 50 | 51 | SchemaEditorClass = DatabaseSchemaEditor 52 | creation_class = DatabaseCreation 53 | features_class = DatabaseFeatures 54 | introspection_class = DatabaseIntrospection 55 | ops_class = DatabaseOperations 56 | client_class = DatabaseClient 57 | 58 | def init_connection_state(self): 59 | super().init_connection_state() 60 | global RAN_TELEMETRY_QUERY 61 | if ( 62 | # Run the telemetry query once, not for every connection. 63 | not RAN_TELEMETRY_QUERY and 64 | # Don't run telemetry if the user disables it... 65 | not getattr(settings, 'DISABLE_COCKROACHDB_TELEMETRY', False) and 66 | # ... or when running Django's test suite. 67 | not os.environ.get('RUNNING_DJANGOS_TEST_SUITE') == 'true' 68 | ): 69 | with self.connection.cursor() as cursor: 70 | cursor.execute( 71 | "SELECT crdb_internal.increment_feature_counter(%s)", 72 | ["django-cockroachdb %s" % django_cockroachdb_version] 73 | ) 74 | RAN_TELEMETRY_QUERY = True 75 | 76 | def check_constraints(self, table_names=None): 77 | """ 78 | Check each table name in `table_names` for rows with invalid foreign 79 | key references. This method is intended to be used in conjunction with 80 | `disable_constraint_checking()` and `enable_constraint_checking()`, to 81 | determine if rows with invalid references were entered while constraint 82 | checks were off. 83 | """ 84 | # cockroachdb doesn't support disabling constraint checking 85 | # (https://github.com/cockroachdb/cockroach/issues/19444) so this 86 | # method is a no-op. 87 | pass 88 | 89 | def chunked_cursor(self): 90 | return self.cursor() 91 | 92 | @contextmanager 93 | def _nodb_cursor(self): 94 | # Overidden to avoid inapplicable "Django was unable to create a 95 | # connection to the 'postgres' database and will use the first 96 | # PostgreSQL database instead." warning. 97 | with super(PostgresDatabaseWrapper, self)._nodb_cursor() as cursor: 98 | yield cursor 99 | 100 | @cached_property 101 | def cockroachdb_server_info(self): 102 | # Something like 'CockroachDB CCL v20.1.0-alpha.20191118-1842-g60d40b8 103 | # (x86_64-unknown-linux-gnu, built 2020/02/03 23:09:23, go1.13.5)'. 104 | with self.temporary_connection() as cursor: 105 | cursor.execute('SELECT VERSION()') 106 | return cursor.fetchone()[0] 107 | 108 | @cached_property 109 | def cockroachdb_version(self): 110 | # Match the numerical portion of the version numbers. For example, 111 | # v20.1.0-alpha.20191118-1842-g60d40b8 returns (20, 1, 0). 112 | match = re.search(r'v(\d{1,2})\.(\d{1,2})\.(\d{1,2})', self.cockroachdb_server_info) 113 | if not match: 114 | raise Exception( 115 | 'Unable to determine CockroachDB version from version ' 116 | 'string %r.' % self.cockroachdb_server_info 117 | ) 118 | return tuple(int(x) for x in match.groups()) 119 | 120 | def get_database_version(self): 121 | return self.cockroachdb_version 122 | -------------------------------------------------------------------------------- /django_cockroachdb/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import subprocess 4 | from urllib.parse import urlencode 5 | 6 | from django.db.backends.base.client import BaseDatabaseClient 7 | 8 | 9 | class DatabaseClient(BaseDatabaseClient): 10 | executable_name = 'cockroach' 11 | 12 | @classmethod 13 | def settings_to_cmd_args_env(cls, settings_dict, parameters): 14 | args = [cls.executable_name, 'sql'] + parameters 15 | db = settings_dict['NAME'] 16 | user = settings_dict['USER'] 17 | password = settings_dict['PASSWORD'] 18 | host = settings_dict['HOST'] 19 | port = settings_dict['PORT'] 20 | sslrootcert = settings_dict['OPTIONS'].get('sslrootcert') 21 | sslcert = settings_dict['OPTIONS'].get('sslcert') 22 | sslkey = settings_dict['OPTIONS'].get('sslkey') 23 | sslmode = settings_dict['OPTIONS'].get('sslmode') 24 | options = settings_dict['OPTIONS'].get('options') 25 | 26 | url_params = {} 27 | if sslrootcert: 28 | url_params["sslrootcert"] = sslrootcert 29 | if sslcert: 30 | url_params["sslcert"] = sslcert 31 | if sslkey: 32 | url_params["sslkey"] = sslkey 33 | if sslmode: 34 | url_params["sslmode"] = sslmode 35 | else: 36 | url_params["sslmode"] = "disable" 37 | if options: 38 | url_params["options"] = options 39 | 40 | environ = os.environ.copy() 41 | query = urlencode(url_params) 42 | environ['COCKROACH_URL'] = f'postgresql://{user}:{password}@{host}:{port}/{db}?{query}' 43 | return args, environ 44 | 45 | def runshell(self, parameters): 46 | args, environ = self.settings_to_cmd_args_env(self.connection.settings_dict, parameters) 47 | sigint_handler = signal.getsignal(signal.SIGINT) 48 | try: 49 | # Allow SIGINT to pass to `cockroach sql` to abort queries. 50 | signal.signal(signal.SIGINT, signal.SIG_IGN) 51 | subprocess.run(args, check=True, env=environ) 52 | finally: 53 | # Restore the original SIGINT handler. 54 | signal.signal(signal.SIGINT, sigint_handler) 55 | -------------------------------------------------------------------------------- /django_cockroachdb/creation.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.postgresql.creation import ( 2 | DatabaseCreation as PostgresDatabaseCreation, 3 | ) 4 | 5 | 6 | class DatabaseCreation(PostgresDatabaseCreation): 7 | 8 | def _clone_test_db(self, suffix, verbosity, keepdb=False): 9 | raise NotImplementedError( 10 | "CockroachDB doesn't support cloning databases. " 11 | "Disable the option to run tests in parallel processes." 12 | ) 13 | -------------------------------------------------------------------------------- /django_cockroachdb/features.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.postgresql.features import ( 2 | DatabaseFeatures as PostgresDatabaseFeatures, 3 | ) 4 | from django.utils.functional import cached_property 5 | 6 | 7 | class DatabaseFeatures(PostgresDatabaseFeatures): 8 | minimum_database_version = (23, 2) 9 | 10 | # Cloning databases doesn't speed up tests. 11 | # https://github.com/cockroachdb/django-cockroachdb/issues/206 12 | can_clone_databases = False 13 | 14 | # Not supported: https://github.com/cockroachdb/cockroach/issues/31632 15 | can_defer_constraint_checks = False 16 | 17 | # Not supported: https://github.com/cockroachdb/cockroach/issues/48307 18 | supports_deferrable_unique_constraints = False 19 | 20 | # There are limitations on having DDL statements in a transaction: 21 | # https://www.cockroachlabs.com/docs/stable/known-limitations.html#schema-changes-within-transactions 22 | can_rollback_ddl = False 23 | 24 | # Not supported: https://github.com/cockroachdb/cockroach/issues/17511 25 | create_test_procedure_without_params_sql = None 26 | create_test_procedure_with_int_param_sql = None 27 | 28 | # Sequences on AutoField don't begin (or reset) to 1 like other databases 29 | # due to use of DEFAULT unique_rowid(). 30 | supports_sequence_reset = False 31 | 32 | # Forward references in fixtures won't work until cockroachdb can 33 | # disable constraints: https://github.com/cockroachdb/cockroach/issues/19444 34 | supports_forward_references = False 35 | 36 | # Unlike PostgreSQL, cockroachdb doesn't support any EXPLAIN formats 37 | # ('JSON', 'TEXT', 'XML', and 'YAML'). 38 | supported_explain_formats = set() 39 | 40 | # Not supported: https://github.com/cockroachdb/cockroach/issues/41645 41 | supports_regex_backreferencing = False 42 | 43 | # CockroachDB sorts NULL values first with ASC and last with DESC. 44 | # PostgreSQL behaves the opposite. 45 | nulls_order_largest = False 46 | 47 | # pg_catalog.obj_description is very slow: 48 | # https://github.com/cockroachdb/cockroach/issues/95068 49 | supports_comments = False 50 | 51 | @cached_property 52 | def introspected_field_types(self): 53 | return { 54 | **super().introspected_field_types, 55 | 'AutoField': 'BigIntegerField', 56 | 'BigAutoField': 'BigIntegerField', 57 | 'IntegerField': 'BigIntegerField', 58 | 'PositiveIntegerField': 'BigIntegerField', 59 | 'SmallAutoField': 'SmallIntegerField', 60 | } 61 | 62 | # CockroachDB doesn't create indexes on foreign keys. 63 | indexes_foreign_keys = False 64 | 65 | # Not supported: https://github.com/cockroachdb/cockroach/issues/59567 66 | supports_non_deterministic_collations = False 67 | 68 | test_collations = { 69 | # PostgresDatabaseFeatures uses 'sv-x-icu' for 'non_default' but 70 | # CockroachDB doesn't introspect that properly: 71 | # https://github.com/cockroachdb/cockroach/issues/54817 72 | 'non_default': 'sv', 73 | 'swedish_ci': 'sv-x-icu', 74 | # Not supported: https://github.com/cockroachdb/cockroach/issues/111091 75 | 'virtual': None, 76 | } 77 | 78 | @cached_property 79 | def is_cockroachdb_24_1(self): 80 | return self.connection.cockroachdb_version >= (24, 1) 81 | 82 | @cached_property 83 | def is_cockroachdb_24_3(self): 84 | return self.connection.cockroachdb_version >= (24, 3) 85 | 86 | @cached_property 87 | def is_cockroachdb_25_1(self): 88 | return self.connection.cockroachdb_version >= (25, 1) 89 | 90 | @cached_property 91 | def django_test_expected_failures(self): 92 | expected_failures = super().django_test_expected_failures 93 | expected_failures.update({ 94 | # sum(): unsupported binary operator: + : 95 | # https://github.com/cockroachdb/django-cockroachdb/issues/73 96 | 'aggregation.tests.AggregateTestCase.test_add_implementation', 97 | 'aggregation.tests.AggregateTestCase.test_combine_different_types', 98 | 'expressions.tests.ExpressionsNumericTests.test_complex_expressions', 99 | # greatest(): expected avg(price) to be of type float, found type 100 | # decimal: https://github.com/cockroachdb/django-cockroachdb/issues/74 101 | 'aggregation.tests.AggregateTestCase.test_expression_on_aggregation', 102 | # POWER() doesn't support negative exponents: 103 | # https://github.com/cockroachdb/django-cockroachdb/issues/22 104 | 'db_functions.math.test_power.PowerTests.test_integer', 105 | # Tests that assume a serial pk: https://github.com/cockroachdb/django-cockroachdb/issues/18 106 | 'multiple_database.tests.RouterTestCase.test_generic_key_cross_database_protection', 107 | # Unsupported query: mixed type addition in SELECT: 108 | # https://github.com/cockroachdb/django-cockroachdb/issues/19 109 | 'annotations.tests.NonAggregateAnnotationTestCase.test_mixed_type_annotation_numbers', 110 | # Forward references in fixtures won't work until CockroachDB can 111 | # disable constraints: https://github.com/cockroachdb/cockroach/issues/19444 112 | 'backends.base.test_creation.TestDeserializeDbFromString.test_circular_reference', 113 | 'backends.base.test_creation.TestDeserializeDbFromString.test_circular_reference_with_natural_key', 114 | 'backends.base.test_creation.TestDeserializeDbFromString.test_self_reference', 115 | 'fixtures.tests.CircularReferenceTests.test_circular_reference', 116 | 'fixtures.tests.ForwardReferenceTests.test_forward_reference_fk', 117 | 'fixtures.tests.ForwardReferenceTests.test_forward_reference_m2m', 118 | 'serializers.test_data.SerializerDataTests.test_json_serializer', 119 | 'serializers.test_data.SerializerDataTests.test_jsonl_serializer', 120 | 'serializers.test_data.SerializerDataTests.test_python_serializer', 121 | 'serializers.test_data.SerializerDataTests.test_xml_serializer', 122 | 'serializers.test_data.SerializerDataTests.test_yaml_serializer', 123 | # No sequence for AutoField in CockroachDB. 124 | 'introspection.tests.IntrospectionTests.test_sequence_list', 125 | # CockroachDB doesn't support disabling constraints: 126 | # https://github.com/cockroachdb/cockroach/issues/19444 127 | 'auth_tests.test_views.UUIDUserTests.test_admin_password_change', 128 | 'backends.tests.FkConstraintsTests.test_check_constraints', 129 | 'backends.tests.FkConstraintsTests.test_check_constraints_sql_keywords', 130 | 'backends.tests.FkConstraintsTests.test_disable_constraint_checks_context_manager', 131 | 'backends.tests.FkConstraintsTests.test_disable_constraint_checks_manually', 132 | # SchemaEditor._model_indexes_sql() doesn't output some expected 133 | # tablespace SQL because CockroachDB automatically indexes foreign 134 | # keys. 135 | 'model_options.test_tablespaces.TablespacesTests.test_tablespace_for_many_to_many_field', 136 | # ALTER COLUMN TYPE requiring rewrite of on-disk data is currently 137 | # not supported for columns that are part of an index. 138 | # https://go.crdb.dev/issue/47636 139 | 'migrations.test_executor.ExecutorTests.test_alter_id_type_with_fk', 140 | 'migrations.test_operations.OperationTests.test_alter_field_pk_fk', 141 | 'migrations.test_operations.OperationTests.test_alter_field_pk_fk_char_to_int', 142 | 'migrations.test_operations.OperationTests.test_alter_field_pk_fk_db_collation', 143 | 'migrations.test_operations.OperationTests.test_alter_field_reloads_state_fk_with_to_field_related_name_target_type_change', # noqa 144 | 'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_with_to_field_target_type_change', # noqa 145 | 'schema.tests.SchemaTests.test_alter_auto_field_to_char_field', 146 | 'schema.tests.SchemaTests.test_alter_autofield_pk_to_smallautofield_pk', 147 | 'schema.tests.SchemaTests.test_alter_primary_key_db_collation', 148 | 'schema.tests.SchemaTests.test_char_field_pk_to_auto_field', 149 | 'schema.tests.SchemaTests.test_char_field_with_db_index_to_fk', 150 | 'schema.tests.SchemaTests.test_text_field_with_db_index_to_fk', 151 | # CockroachDB doesn't support dropping the primary key. 152 | 'schema.tests.SchemaTests.test_alter_int_pk_to_int_unique', 153 | # unimplemented: primary key dropped without subsequent addition of 154 | # new primary key in same transaction 155 | # https://github.com/cockroachdb/cockroach/issues/48026 156 | 'migrations.test_operations.OperationTests.test_composite_pk_operations', 157 | # CockroachDB doesn't support changing the primary key of table. 158 | # psycopg.errors.InvalidColumnReference: column "id" is referenced 159 | # by the primary key 160 | 'migrations.test_operations.OperationTests.test_alter_id_pk_to_uuid_pk', 161 | 'schema.tests.SchemaTests.test_alter_not_unique_field_to_primary_key', 162 | 'schema.tests.SchemaTests.test_primary_key', 163 | # SmallAutoField doesn't work: 164 | # https://github.com/cockroachdb/cockroach-django/issues/84 165 | 'bulk_create.tests.BulkCreateTests.test_bulk_insert_nullable_fields', 166 | 'many_to_one.tests.ManyToOneTests.test_add_remove_set_by_pk_raises', 167 | 'many_to_one.tests.ManyToOneTests.test_fk_to_smallautofield', 168 | 'many_to_one.tests.ManyToOneTests.test_get_prefetch_queryset_reverse_warning', 169 | 'many_to_one.tests.ManyToOneTests.test_get_prefetch_querysets_reverse_invalid_querysets_length', 170 | 'migrations.test_operations.OperationTests.test_smallfield_autofield_foreignfield_growth', 171 | 'migrations.test_operations.OperationTests.test_smallfield_bigautofield_foreignfield_growth', 172 | 'schema.tests.SchemaTests.test_alter_smallint_pk_to_smallautofield_pk', 173 | # unexpected unique index in pg_constraint query: 174 | # https://github.com/cockroachdb/cockroach/issues/61098 175 | 'introspection.tests.IntrospectionTests.test_get_constraints_unique_indexes_orders', 176 | 'schema.tests.SchemaTests.test_func_unique_constraint', 177 | 'schema.tests.SchemaTests.test_func_unique_constraint_collate', 178 | 'schema.tests.SchemaTests.test_func_unique_constraint_covering', 179 | 'schema.tests.SchemaTests.test_unique_constraint_field_and_expression', 180 | # unimplemented: primary key dropped without subsequent addition of 181 | # new primary key in same transaction 182 | 'schema.tests.SchemaTests.test_add_auto_field', 183 | 'schema.tests.SchemaTests.test_autofield_to_o2o', 184 | # incompatible COALESCE expressions: unsupported binary operator: 185 | # * (desired ): 186 | # https://github.com/cockroachdb/cockroach/issues/73587 187 | 'aggregation.tests.AggregateTestCase.test_aggregation_default_expression', 188 | # ProgrammingError: VALUES types int and float cannot be matched 189 | 'field_defaults.tests.DefaultTests.test_bulk_create_mixed_db_defaults_function', 190 | }) 191 | if not self.is_cockroachdb_24_3: 192 | expected_failures.update({ 193 | # ALTER COLUMN TYPE requiring rewrite of on-disk data is currently 194 | # not supported for columns that are part of an index. 195 | # https://go.crdb.dev/issue/47636 196 | 'schema.tests.SchemaTests.test_alter_primary_key_the_same_name', 197 | 'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_target_changes', 198 | 'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_with_to_field_target_changes', # noqa 199 | 'migrations.test_operations.OperationTests.test_rename_field_reloads_state_on_fk_target_changes', 200 | # unknown signature: concat(varchar, int) (returning ) 201 | 'migrations.test_operations.OperationTests.test_add_generated_field', 202 | # concat(): unknown signature: concat(string, int2) (desired ) 203 | 'db_functions.text.test_concat.ConcatTests.test_concat_non_str', 204 | }) 205 | if self.is_cockroachdb_24_1: 206 | # USING cast required: https://github.com/cockroachdb/cockroach/issues/82416#issuecomment-2029803229 207 | expected_failures.update({ 208 | 'schema.tests.SchemaTests.test_alter_text_field_to_date_field', 209 | 'schema.tests.SchemaTests.test_alter_text_field_to_datetime_field', 210 | 'schema.tests.SchemaTests.test_alter_text_field_to_time_field', 211 | }) 212 | if self.is_cockroachdb_25_1: 213 | expected_failures.update({ 214 | # expected STORED COMPUTED COLUMN expression to have type 215 | # decimal, but 'pink + pink' has type int 216 | 'migrations.test_operations.OperationTests.test_generated_field_changes_output_field', 217 | }) 218 | if self.uses_server_side_binding: 219 | expected_failures.update({ 220 | # could not determine data type of placeholder: 221 | # https://github.com/cockroachdb/cockroach/issues/91396 222 | 'backends.tests.EscapingChecks.test_parameter_escaping', 223 | 'backends.tests.EscapingChecksDebug.test_parameter_escaping', 224 | 'composite_pk.test_update.CompositePKUpdateTests.test_bulk_update_comments', 225 | 'constraints.tests.CheckConstraintTests.test_database_default', 226 | 'expressions.tests.BasicExpressionsTests.test_annotate_values_filter', 227 | 'expressions_case.tests.CaseDocumentationExamples.test_lookup_example', 228 | 'expressions_case.tests.CaseDocumentationExamples.test_simple_example', 229 | 'expressions_case.tests.CaseExpressionTests.test_aggregation_empty_cases', 230 | 'expressions_case.tests.CaseExpressionTests.test_annotate', 231 | 'expressions_case.tests.CaseExpressionTests.test_annotate_exclude', 232 | 'expressions_case.tests.CaseExpressionTests.test_annotate_values_not_in_order_by', 233 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_aggregation_in_condition', 234 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_aggregation_in_predicate', 235 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_annotation_in_condition', 236 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_annotation_in_predicate', 237 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_empty_when', 238 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_expression_as_condition', 239 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_full_when', 240 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_join_in_condition', 241 | 'expressions_case.tests.CaseExpressionTests.test_annotate_with_join_in_predicate', 242 | 'expressions_case.tests.CaseExpressionTests.test_case_reuse', 243 | 'expressions_case.tests.CaseExpressionTests.test_combined_q_object', 244 | 'expressions_case.tests.CaseExpressionTests.test_lookup_different_fields', 245 | 'expressions_case.tests.CaseExpressionTests.test_lookup_in_condition', 246 | 'expressions_case.tests.CaseExpressionTests.test_update_generic_ip_address', 247 | 'lookup.tests.LookupQueryingTests.test_conditional_expression', 248 | 'ordering.tests.OrderingTests.test_order_by_constant_value', 249 | 'queries.test_bulk_update.BulkUpdateNoteTests.test_batch_size', 250 | 'queries.test_bulk_update.BulkUpdateNoteTests.test_multiple_fields', 251 | 'queries.test_bulk_update.BulkUpdateNoteTests.test_simple', 252 | 'queries.test_bulk_update.BulkUpdateTests.test_custom_pk', 253 | 'queries.test_bulk_update.BulkUpdateTests.test_database_routing', 254 | 'queries.test_bulk_update.BulkUpdateTests.test_database_routing_batch_atomicity', 255 | 'queries.test_bulk_update.BulkUpdateTests.test_falsey_pk_value', 256 | 'queries.test_bulk_update.BulkUpdateTests.test_inherited_fields', 257 | 'queries.test_bulk_update.BulkUpdateTests.test_large_batch', 258 | 'queries.test_bulk_update.BulkUpdateTests.test_updated_rows_when_passing_duplicates', 259 | 'queries.test_q.QCheckTests.test_expression', 260 | 'queries.test_qs_combinators.QuerySetSetOperationTests.test_union_multiple_models_with_values_list_and_annotations', # noqa 261 | # error in argument for $2: could not parse ":" as type int2: 262 | # strconv.ParseInt: parsing ":": invalid syntax 263 | # https://github.com/cockroachdb/cockroach/issues/136295 264 | 'db_functions.text.test_concat.ConcatTests.test_concat_non_str', 265 | # unsupported binary operator: / 266 | 'expressions.tests.FTimeDeltaTests.test_durationfield_multiply_divide', 267 | # InvalidParameterValue: unsupported binary operator: / 268 | 'queries.tests.Ticket23605Tests.test_ticket_23605', 269 | # InvalidParameterValue: unsupported binary operator: + 270 | 'annotations.tests.NonAggregateAnnotationTestCase.test_combined_annotation_commutative', 271 | # incompatible COALESCE expressions: unsupported binary 272 | # operator: / (desired ) 273 | 'aggregation.tests.AggregateTestCase.test_aggregation_default_passed_another_aggregate', 274 | }) 275 | if self.is_cockroachdb_24_3: 276 | expected_failures.update({ 277 | # psycopg.errors.IndeterminateDatatype: replace(): 278 | # replace(): replace(): concat(): could not determine data 279 | # type of placeholder $3. This worked until v24.3 added 280 | # support for non-string data to concat(): 281 | # https://github.com/cockroachdb/cockroach/pull/127098#issuecomment-2492652084 282 | "model_fields.test_uuid.TestQuerying.test_filter_with_expr", 283 | }) 284 | if self.is_cockroachdb_25_1: 285 | expected_failures.update({ 286 | # psycopg.errors.IndeterminateDatatype: could not determine 287 | # data type of placeholder $1 288 | 'expressions_case.tests.CaseExpressionTests.test_filter_with_expression_as_condition', 289 | }) 290 | else: 291 | expected_failures.update({ 292 | # Unsupported query: unsupported binary operator: / : 293 | # https://github.com/cockroachdb/django-cockroachdb/issues/21 294 | 'expressions.tests.ExpressionOperatorTests.test_lefthand_division', 295 | 'expressions.tests.ExpressionOperatorTests.test_right_hand_division', 296 | }) 297 | return expected_failures 298 | 299 | @cached_property 300 | def django_test_skips(self): 301 | skips = super().django_test_skips 302 | skips.update({ 303 | # https://github.com/cockroachdb/django-cockroachdb/issues/153#issuecomment-664697963 304 | 'CockroachDB has more restrictive blocking than other databases.': { 305 | 'select_for_update.tests.SelectForUpdateTests.test_block', 306 | }, 307 | # https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference.html#retry_write_too_old 308 | 'Fails with TransactionRetryWithProtoRefreshError: ... RETRY_WRITE_TOO_OLD ...': { 309 | 'delete_regress.tests.DeleteLockingTest.test_concurrent_delete', 310 | }, 311 | 'Skip to prevents some error output in the logs.': { 312 | # Since QuerySet.select_for_update() was enabled, this test is 313 | # already skipped by the 'Database took too long to lock the row' 314 | # logic in the test. Skipping it entirely prevents some error 315 | # output in the logs: 316 | # Exception in thread Thread-1: 317 | # ... 318 | # psycopg.errors.SerializationFailure: restart transaction: 319 | # TransactionRetryWithProtoRefreshError: WriteTooOldError: write 320 | # at timestamp 1598314405.858850941,0 too old; wrote at 321 | # 1598314405.883337663,1 322 | 'get_or_create.tests.UpdateOrCreateTransactionTests.test_creation_in_transaction', 323 | # Sometimes fails as above or with 324 | # AssertionError: datetime.timedelta(microseconds=28529) not 325 | # greater than datetime.timedelta(microseconds=500000) 326 | 'get_or_create.tests.UpdateOrCreateTransactionTests.test_updates_in_transaction', 327 | }, 328 | # https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference.html#abort_reason_client_reject 329 | 'Often fails with SerializationFailure: restart transaction: TransactionRetryWithProtoRefreshError': { 330 | 'admin_views.test_multidb.ViewOnSiteTests.test_contenttype_in_separate_db', 331 | }, 332 | }) 333 | if not self.is_cockroachdb_25_1: 334 | skips.update({ 335 | # https://github.com/cockroachdb/cockroach/issues/47137 336 | # These tests only fail sometimes, e.g. 337 | # https://github.com/cockroachdb/cockroach/issues/65691 338 | 'ALTER COLUMN fails if previous asynchronous ALTER COLUMN has not finished.': { 339 | 'schema.tests.SchemaTests.test_alter_field_db_collation', 340 | 'schema.tests.SchemaTests.test_alter_field_type_and_db_collation', 341 | }, 342 | }) 343 | return skips 344 | -------------------------------------------------------------------------------- /django_cockroachdb/functions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db.models import ( 4 | DateTimeField, DecimalField, FloatField, IntegerField, 5 | ) 6 | from django.db.models.expressions import When 7 | from django.db.models.functions import ( 8 | ACos, ASin, ATan, ATan2, Cast, Ceil, Coalesce, Collate, Cos, Cot, Degrees, 9 | Exp, Floor, JSONArray, JSONObject, Ln, Log, Radians, Round, Sin, Sqrt, 10 | StrIndex, Tan, 11 | ) 12 | 13 | 14 | def coalesce(self, compiler, connection, **extra_context): 15 | # When coalescing a timestamptz column and a Python datetime, the datetime 16 | # must be cast to timestamptz (DateTimeField) to avoid "incompatible 17 | # COALESCE expressions: expected 'YYYY-MM-DDTHH:MM:SS'::TIMESTAMP to be of 18 | # type timestamptz, found type timestamp". 19 | if self.output_field.get_internal_type() == 'DateTimeField': 20 | clone = self.copy() 21 | clone.set_source_expressions([ 22 | Cast(expression, DateTimeField()) for expression in self.get_source_expressions() 23 | ]) 24 | return super(Coalesce, clone).as_sql(compiler, connection, **extra_context) 25 | return self.as_sql(compiler, connection, **extra_context) 26 | 27 | 28 | def collate(self, compiler, connection, **extra_context): 29 | return self.as_sql( 30 | compiler, connection, 31 | # CockroachDB requires parentheses around the expression in 32 | # CREATE INDEX: https://github.com/cockroachdb/cockroach/issues/71240 33 | template='(%(expressions)s %(function)s %(collation)s)', 34 | **extra_context 35 | ) 36 | 37 | 38 | def float_cast(self, compiler, connection, **extra_context): 39 | # Most cockroachdb math functions require float arguments instead of 40 | # decimal or integer. 41 | clone = self.copy() 42 | clone.set_source_expressions([ 43 | Cast(expression, FloatField()) if isinstance(expression.output_field, (DecimalField, IntegerField)) 44 | else expression for expression in self.get_source_expressions() 45 | ]) 46 | return clone.as_sql(compiler, connection, **extra_context) 47 | 48 | 49 | def round_cast(self, compiler, connection, **extra_context): 50 | # ROUND() doesn't accept integer values. Cast to decimal (rather than 51 | # float) so that half away from zero rounding is used, consistent with 52 | # other databases (rather than half to even rounding). 53 | clone = self.copy() 54 | value, precision = self.get_source_expressions() 55 | value = ( 56 | Cast(value, DecimalField(max_digits=2147481649, decimal_places=0)) 57 | if isinstance(value.output_field, IntegerField) 58 | else value 59 | ) 60 | clone.set_source_expressions([value, precision]) 61 | return clone.as_sql(compiler, connection, **extra_context) 62 | 63 | 64 | def when(self, compiler, connection, **extra_context): 65 | # As for coalesce(), cast datetimes to timestamptz. 66 | if isinstance(getattr(self.result, 'value', None), datetime.datetime): 67 | self.result = Cast(self.result, DateTimeField()) 68 | return self.as_sql(compiler, connection, **extra_context) 69 | 70 | 71 | def register_functions(): 72 | math_funcs_needing_float_cast = ( 73 | ACos, ASin, ATan, ATan2, Ceil, Cos, Cot, Degrees, Exp, Floor, Ln, Log, 74 | Radians, Sin, Sqrt, Tan, 75 | ) 76 | for func in math_funcs_needing_float_cast: 77 | func.as_cockroachdb = float_cast 78 | Coalesce.as_cockroachdb = coalesce 79 | Collate.as_cockroachdb = collate 80 | JSONArray.as_cockroachdb = JSONArray.as_postgresql 81 | JSONObject.as_cockroachdb = JSONObject.as_postgresql 82 | Round.as_cockroachdb = round_cast 83 | StrIndex.as_cockroachdb = StrIndex.as_postgresql 84 | When.as_cockroachdb = when 85 | -------------------------------------------------------------------------------- /django_cockroachdb/introspection.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.postgresql.introspection import ( 2 | DatabaseIntrospection as PostgresDatabaseIntrospection, TableInfo, 3 | ) 4 | 5 | 6 | class DatabaseIntrospection(PostgresDatabaseIntrospection): 7 | data_types_reverse = dict(PostgresDatabaseIntrospection.data_types_reverse) 8 | data_types_reverse[1184] = 'DateTimeField' # TIMESTAMPTZ 9 | index_default_access_method = 'prefix' 10 | 11 | def get_table_list(self, cursor): 12 | # pg_catalog.obj_description is removed from this query to speed it up: 13 | # https://github.com/cockroachdb/cockroach/issues/95068 14 | """Return a list of table and view names in the current database.""" 15 | cursor.execute( 16 | """ 17 | SELECT 18 | c.relname, 19 | CASE 20 | WHEN c.relispartition THEN 'p' 21 | WHEN c.relkind IN ('m', 'v') THEN 'v' 22 | ELSE 't' 23 | END, 24 | '' 25 | FROM pg_catalog.pg_class c 26 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 27 | WHERE c.relkind IN ('f', 'm', 'p', 'r', 'v') 28 | AND n.nspname NOT IN ('pg_catalog', 'pg_toast') 29 | AND pg_catalog.pg_table_is_visible(c.oid) 30 | """ 31 | ) 32 | return [ 33 | TableInfo(*row) 34 | for row in cursor.fetchall() 35 | if row[0] not in self.ignored_tables 36 | ] 37 | -------------------------------------------------------------------------------- /django_cockroachdb/lookups.py: -------------------------------------------------------------------------------- 1 | from django.db.models.fields.json import HasKeyLookup, KeyTransform 2 | from django.db.models.lookups import PostgresOperatorLookup 3 | 4 | 5 | def patch_lookups(): 6 | HasKeyLookup.as_cockroachdb = HasKeyLookup.as_postgresql 7 | KeyTransform.as_cockroachdb = KeyTransform.as_postgresql 8 | PostgresOperatorLookup.as_cockroachdb = PostgresOperatorLookup.as_postgresql 9 | -------------------------------------------------------------------------------- /django_cockroachdb/operations.py: -------------------------------------------------------------------------------- 1 | import time 2 | from zoneinfo import ZoneInfo 3 | 4 | from django.db.backends.postgresql.operations import ( 5 | DatabaseOperations as PostgresDatabaseOperations, 6 | ) 7 | from django.db.backends.postgresql.psycopg_any import errors, is_psycopg3 8 | from django.db.utils import OperationalError 9 | 10 | 11 | class DatabaseOperations(PostgresDatabaseOperations): 12 | integer_field_ranges = { 13 | 'SmallIntegerField': (-32768, 32767), 14 | 'IntegerField': (-9223372036854775808, 9223372036854775807), 15 | 'BigIntegerField': (-9223372036854775808, 9223372036854775807), 16 | 'PositiveSmallIntegerField': (0, 32767), 17 | 'PositiveBigIntegerField': (0, 9223372036854775807), 18 | 'PositiveIntegerField': (0, 9223372036854775807), 19 | 'SmallAutoField': (-32768, 32767), 20 | 'AutoField': (-9223372036854775808, 9223372036854775807), 21 | 'BigAutoField': (-9223372036854775808, 9223372036854775807), 22 | } 23 | 24 | if is_psycopg3: 25 | from psycopg.types import numeric 26 | 27 | integerfield_type_map = { 28 | "SmallIntegerField": numeric.Int2, 29 | "IntegerField": numeric.Int8, 30 | "BigIntegerField": numeric.Int8, 31 | "PositiveSmallIntegerField": numeric.Int2, 32 | "PositiveIntegerField": numeric.Int8, 33 | "PositiveBigIntegerField": numeric.Int8, 34 | } 35 | 36 | explain_options = frozenset(['DISTSQL', 'OPT', 'TYPES', 'VEC', 'VERBOSE']) 37 | 38 | def deferrable_sql(self): 39 | # Deferrable constraints aren't supported: 40 | # https://github.com/cockroachdb/cockroach/issues/31632 41 | return '' 42 | 43 | def adapt_datetimefield_value(self, value): 44 | """ 45 | Add a timezone to datetimes so that psycopg will cast it to 46 | TIMESTAMPTZ (as cockroach expects) rather than TIMESTAMP. 47 | """ 48 | # getattr() guards against F() objects which don't have tzinfo. 49 | if value and getattr(value, 'tzinfo', '') is None and self.connection.timezone_name is not None: 50 | connection_timezone = ZoneInfo(self.connection.timezone_name) 51 | value = value.replace(tzinfo=connection_timezone) 52 | return value 53 | 54 | def sequence_reset_by_name_sql(self, style, sequences): 55 | # Not implemented: https://github.com/cockroachdb/cockroach/issues/20956 56 | return [] 57 | 58 | def sequence_reset_sql(self, style, model_list): 59 | return [] 60 | 61 | def explain_query_prefix(self, format=None, **options): 62 | extra = [] 63 | # Normalize options. 64 | if options: 65 | options = { 66 | name.upper(): value 67 | for name, value in options.items() 68 | } 69 | for valid_option in self.explain_options: 70 | value = options.pop(valid_option, None) 71 | if value: 72 | extra.append(valid_option) 73 | prefix = super().explain_query_prefix(format, **options) 74 | if extra: 75 | prefix += ' (%s)' % ', '.join(extra) 76 | return prefix 77 | 78 | def execute_sql_flush(self, sql_list): 79 | # Retry TRUNCATE if it fails with a serialization error. 80 | num_retries = 10 81 | initial_retry_delay = 0.5 # The initial retry delay, in seconds. 82 | backoff_ = 1.5 # For each retry, the last delay is multiplied by this. 83 | next_retry_delay = initial_retry_delay 84 | for retry in range(1, num_retries + 1): 85 | try: 86 | return super().execute_sql_flush(sql_list) 87 | except OperationalError as exc: 88 | if (not isinstance(exc.__cause__, errors.SerializationFailure) or 89 | retry >= num_retries): 90 | raise 91 | time.sleep(next_retry_delay) 92 | next_retry_delay *= backoff_ 93 | 94 | def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): 95 | # CockroachDB doesn't support resetting sequences. 96 | return super().sql_flush(style, tables, reset_sequences=False, allow_cascade=allow_cascade) 97 | -------------------------------------------------------------------------------- /django_cockroachdb/schema.py: -------------------------------------------------------------------------------- 1 | from django.db.backends.base.schema import BaseDatabaseSchemaEditor 2 | from django.db.backends.postgresql.schema import ( 3 | DatabaseSchemaEditor as PostgresDatabaseSchemaEditor, 4 | ) 5 | from django.db.backends.utils import strip_quotes 6 | from django.db.models import ForeignKey 7 | 8 | 9 | class DatabaseSchemaEditor(PostgresDatabaseSchemaEditor): 10 | # The PostgreSQL backend uses "SET CONSTRAINTS ... IMMEDIATE" before 11 | # "ALTER TABLE..." to run any any deferred checks to allow dropping the 12 | # foreign key in the same transaction. This doesn't apply to cockroachdb. 13 | sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" 14 | 15 | # "ALTER TABLE ... DROP CONSTRAINT ..." not supported for dropping UNIQUE 16 | # constraints; must use this instead. 17 | sql_delete_unique = "DROP INDEX %(name)s CASCADE" 18 | 19 | # The PostgreSQL backend uses "SET CONSTRAINTS ... IMMEDIATE" after 20 | # creating this foreign key. This isn't supported by CockroachDB. 21 | sql_create_column_inline_fk = 'CONSTRAINT %(name)s REFERENCES %(to_table)s(%(to_column)s)%(deferrable)s' 22 | 23 | # The PostgreSQL backend uses "SET CONSTRAINTS ... IMMEDIATE" after this 24 | # statement. This isn't supported by CockroachDB. 25 | sql_update_with_default = "UPDATE %(table)s SET %(column)s = %(default)s WHERE %(column)s IS NULL" 26 | 27 | def __enter__(self): 28 | super().__enter__() 29 | # As long as DatabaseFeatures.can_rollback_ddl = False, compose() may 30 | # fail if connection is None as per 31 | # https://github.com/django/django/pull/15687#discussion_r1038175823. 32 | # See also https://github.com/django/django/pull/15687#discussion_r1041503991. 33 | self.connection.ensure_connection() 34 | return self 35 | 36 | def add_index(self, model, index, concurrently=False): 37 | if index.contains_expressions and not self.connection.features.supports_expression_indexes: 38 | return None 39 | super().add_index(model, index, concurrently) 40 | 41 | def remove_index(self, model, index, concurrently=False): 42 | if index.contains_expressions and not self.connection.features.supports_expression_indexes: 43 | return None 44 | super().remove_index(model, index, concurrently) 45 | 46 | def _index_columns(self, table, columns, col_suffixes, opclasses): 47 | # cockroachdb doesn't support PostgreSQL opclasses. 48 | return BaseDatabaseSchemaEditor._index_columns(self, table, columns, col_suffixes, opclasses) 49 | 50 | def _create_like_index_sql(self, model, field): 51 | # cockroachdb doesn't support LIKE indexes. 52 | return None 53 | 54 | def _alter_field(self, model, old_field, new_field, old_type, new_type, 55 | old_db_params, new_db_params, strict=False): 56 | # ALTER COLUMN TYPE is experimental. 57 | # https://github.com/cockroachdb/cockroach/issues/49329 58 | if ( 59 | not self.connection.features.is_cockroachdb_25_1 and ( 60 | old_type != new_type or 61 | getattr(old_field, 'db_collation', None) != getattr(new_field, 'db_collation', None) 62 | ) 63 | ): 64 | self.execute('SET enable_experimental_alter_column_type_general = true') 65 | # Skip to the base class to avoid trying to add or drop 66 | # PostgreSQL-specific LIKE indexes. 67 | BaseDatabaseSchemaEditor._alter_field( 68 | self, model, old_field, new_field, old_type, new_type, old_db_params, 69 | new_db_params, strict, 70 | ) 71 | # Add or remove `DEFAULT unique_rowid()` for AutoField. 72 | old_suffix = old_field.db_type_suffix(self.connection) 73 | new_suffix = new_field.db_type_suffix(self.connection) 74 | if old_suffix != new_suffix: 75 | if new_suffix: 76 | self.execute(self.sql_alter_column % { 77 | 'table': self.quote_name(model._meta.db_table), 78 | 'changes': 'ALTER COLUMN %(column)s SET %(expression)s' % { 79 | 'column': self.quote_name(new_field.column), 80 | 'expression': new_suffix, 81 | } 82 | }) 83 | else: 84 | self.execute(self.sql_alter_column % { 85 | 'table': self.quote_name(model._meta.db_table), 86 | 'changes': 'ALTER COLUMN %(column)s DROP DEFAULT' % { 87 | 'column': self.quote_name(new_field.column), 88 | } 89 | }) 90 | 91 | def _alter_column_type_sql(self, model, old_field, new_field, new_type, old_collation, new_collation): 92 | new_internal_type = new_field.get_internal_type() 93 | old_internal_type = old_field.get_internal_type() 94 | # Make ALTER TYPE with AutoField make sense. 95 | auto_field_types = {'AutoField', 'BigAutoField', 'SmallAutoField'} 96 | old_is_auto = old_internal_type in auto_field_types 97 | new_is_auto = new_internal_type in auto_field_types 98 | if new_is_auto and not old_is_auto: 99 | column = strip_quotes(new_field.column) 100 | return ( 101 | ( 102 | self.sql_alter_column_type % { 103 | "column": self.quote_name(column), 104 | "type": new_type, 105 | "collation": "", 106 | }, 107 | [], 108 | ), 109 | # The PostgreSQL backend manages the column's identity here but 110 | # this isn't applicable on CockroachDB because unique_rowid() 111 | # is used instead. 112 | [], 113 | ) 114 | else: 115 | return BaseDatabaseSchemaEditor._alter_column_type_sql( 116 | self, model, old_field, new_field, new_type, 117 | old_collation, new_collation, 118 | ) 119 | 120 | def _field_should_be_indexed(self, model, field): 121 | # Foreign keys are automatically indexed by cockroachdb. 122 | return not isinstance(field, ForeignKey) and super()._field_should_be_indexed(model, field) 123 | -------------------------------------------------------------------------------- /django_cockroachdb/utils.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.utils.version import get_version_tuple 4 | 5 | 6 | def check_django_compatability(): 7 | """ 8 | Verify that this version of django-cockroachdb is compatible with the 9 | installed version of Django. For example, any django-cockroachdb 2.2.x is 10 | compatible with Django 2.2.y. 11 | """ 12 | from . import __version__ 13 | if django.VERSION[:2] != get_version_tuple(__version__)[:2]: 14 | raise ImproperlyConfigured( 15 | 'You must use the latest version of django-cockroachdb {A}.{B}.x ' 16 | 'with Django {A}.{B}.y (found django-cockroachdb {C}).'.format( 17 | A=django.VERSION[0], 18 | B=django.VERSION[1], 19 | C=__version__, 20 | ) 21 | ) 22 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/__init__.py: -------------------------------------------------------------------------------- 1 | from .functions import register_functions 2 | 3 | register_functions() 4 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.base import ( 2 | DatabaseWrapper as PostGISDatabaseWrapper, 3 | ) 4 | 5 | from django_cockroachdb.base import DatabaseWrapper as CockroachDatabaseWrapper 6 | 7 | from .features import DatabaseFeatures 8 | from .introspection import DatabaseIntrospection 9 | from .operations import DatabaseOperations 10 | from .schema import DatabaseSchemaEditor 11 | 12 | 13 | class DatabaseWrapper(CockroachDatabaseWrapper, PostGISDatabaseWrapper): 14 | SchemaEditorClass = DatabaseSchemaEditor 15 | features_class = DatabaseFeatures 16 | introspection_class = DatabaseIntrospection 17 | ops_class = DatabaseOperations 18 | 19 | def __init__(self, *args, **kwargs): 20 | # Skip PostGISDatabaseWrapper.__init__() to work around 21 | # https://code.djangoproject.com/ticket/34344. 22 | CockroachDatabaseWrapper.__init__(self, *args, **kwargs) 23 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/features.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.features import ( 2 | DatabaseFeatures as PostGISFeatures, 3 | ) 4 | from django.utils.functional import cached_property 5 | 6 | from django_cockroachdb.features import DatabaseFeatures as CockroachFeatures 7 | 8 | 9 | class DatabaseFeatures(CockroachFeatures, PostGISFeatures): 10 | supports_raster = False 11 | # Not supported: https://github.com/cockroachdb/cockroach/issues/57092 12 | supports_left_right_lookups = False 13 | # unimplemented: column point is of type geometry and thus is not indexable 14 | # https://go.crdb.dev/issue/35730 15 | supports_geometry_field_unique_index = False 16 | 17 | @cached_property 18 | def django_test_expected_failures(self): 19 | expected_failures = super().django_test_expected_failures 20 | expected_failures.update({ 21 | # ST_AsText output different from PostGIS (extra space): 22 | # https://github.com/cockroachdb/cockroach/issues/53651 23 | 'gis_tests.geoapp.test_functions.GISFunctionsTests.test_aswkt', 24 | # Unsupported ~= (same_as/exact) operator: 25 | # https://github.com/cockroachdb/cockroach/issues/57096 26 | 'gis_tests.geoapp.tests.GeoLookupTest.test_equals_lookups', 27 | 'gis_tests.geoapp.tests.GeoLookupTest.test_null_geometries_excluded_in_lookups', 28 | 'gis_tests.gis_migrations.test_operations.OperationTests.test_add_check_constraint', 29 | 'gis_tests.relatedapp.tests.RelatedGeoModelTest.test06_f_expressions', 30 | # unknown signature: st_union(geometry, geometry) 31 | # https://github.com/cockroachdb/cockroach/issues/49064 32 | 'gis_tests.distapp.tests.DistanceTest.test_dwithin', 33 | 'gis_tests.geoapp.test_functions.GISFunctionsTests.test_diff_intersection_union', 34 | 'gis_tests.geoapp.test_functions.GISFunctionsTests.test_union_mixed_srid', 35 | 'gis_tests.geoapp.test_functions.GISFunctionsTests.test_union', 36 | 'gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions', 37 | 'gis_tests.geoapp.tests.GeoLookupTest.test_relate_lookup', 38 | # NotSupportedError: this box2d comparison operator is experimental 39 | 'gis_tests.geoapp.tests.GeoLookupTest.test_contains_contained_lookups', 40 | # unknown signature: st_distancespheroid(geometry, geometry, string) 41 | # https://github.com/cockroachdb/cockroach/issues/48922#issuecomment-693096502 42 | 'gis_tests.distapp.tests.DistanceTest.test_distance_lookups_with_expression_rhs', 43 | 'gis_tests.distapp.tests.DistanceTest.test_geodetic_distance_lookups', 44 | 'gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_geodetic_spheroid', 45 | # st_lengthspheroid(): unimplemented: 46 | # https://github.com/cockroachdb/cockroach/issues/48968 47 | 'gis_tests.distapp.tests.DistanceFunctionsTests.test_length', 48 | # Unsupported ~= (https://github.com/cockroachdb/cockroach/issues/57096) 49 | # and @ operators (https://github.com/cockroachdb/cockroach/issues/56124). 50 | 'gis_tests.geogapp.tests.GeographyTest.test_operators_functions_unavailable_for_geography', 51 | # unknown function: st_3dperimeter 52 | # https://github.com/cockroachdb/cockroach/issues/60871 53 | 'gis_tests.geo3d.tests.Geo3DFunctionsTests.test_perimeter', 54 | # unknown function: st_3dextent() 55 | # https://github.com/cockroachdb/cockroach/issues/60864 56 | 'gis_tests.geo3d.tests.Geo3DTest.test_extent', 57 | 'gis_tests.geo3d.tests.Geo3DTest.test_extent3d_filter', 58 | # unknown signature: st_scale(geometry, decimal, decimal, decimal) 59 | # https://github.com/cockroachdb/cockroach/issues/49027 60 | 'gis_tests.geo3d.tests.Geo3DFunctionsTests.test_scale', 61 | # ASKML not supported: 62 | # https://github.com/cockroachdb/cockroach/issues/48881 63 | 'gis_tests.geo3d.tests.Geo3DFunctionsTests.test_kml', 64 | # st_lengthspheroid(): unimplemented: 65 | # https://github.com/cockroachdb/cockroach/issues/48968 66 | 'gis_tests.geo3d.tests.Geo3DFunctionsTests.test_length', 67 | # unimplemented: ALTER COLUMN TYPE requiring rewrite of on-disk 68 | # data is currently not supported for columns that are part of an 69 | # index: https://github.com/cockroachdb/cockroach/issues/47636 70 | 'gis_tests.gis_migrations.test_operations.OperationTests.test_alter_geom_field_dim', 71 | 'gis_tests.gis_migrations.test_operations.OperationTests.test_alter_field_with_spatial_index', 72 | # 3D opclass not present on CockroachDB: 73 | # https://github.com/cockroachdb/cockroach/issues/47420#issuecomment-969578772 74 | 'gis_tests.gis_migrations.test_operations.OperationTests.test_add_3d_field_opclass', 75 | }) 76 | if self.uses_server_side_binding: 77 | expected_failures.update({ 78 | # unknown signature: st_scale(geometry, int, int) 79 | 'gis_tests.geoapp.test_functions.GISFunctionsTests.test_scale', 80 | }) 81 | else: 82 | expected_failures.update({ 83 | # unknown signature: st_dwithin(geography, geometry, decimal) (desired ) 84 | # https://github.com/cockroachdb/cockroach/issues/53720 85 | 'gis_tests.geogapp.tests.GeographyTest.test02_distance_lookup', 86 | }) 87 | return expected_failures 88 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/functions.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.models.functions import Distance, Length, Perimeter 2 | 3 | 4 | def register_functions(): 5 | Distance.as_cockroachdb = Distance.as_postgresql 6 | Length.as_cockroachdb = Length.as_postgresql 7 | Perimeter.as_cockroachdb = Perimeter.as_postgresql 8 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/introspection.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.introspection import ( 2 | PostGISIntrospection, 3 | ) 4 | 5 | from django_cockroachdb.introspection import ( 6 | DatabaseIntrospection as CockroachIntrospection, 7 | ) 8 | 9 | 10 | class DatabaseIntrospection(CockroachIntrospection, PostGISIntrospection): 11 | pass 12 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/operations.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.operations import PostGISOperations 2 | 3 | from django_cockroachdb.operations import ( 4 | DatabaseOperations as CockroachOperations, 5 | ) 6 | 7 | 8 | class DatabaseOperations(CockroachOperations, PostGISOperations): 9 | 10 | @property 11 | def gis_operators(self): 12 | ops = PostGISOperations.gis_operators.copy() 13 | # https://github.com/cockroachdb/cockroach/issues/56124 14 | del ops['contained'] # @ 15 | # https://github.com/cockroachdb/cockroach/issues/57096 16 | del ops['exact'] # ~= 17 | del ops['same_as'] # ~= 18 | # https://github.com/cockroachdb/cockroach/issues/57092 19 | del ops['left'] # << 20 | del ops['right'] # >> 21 | # https://github.com/cockroachdb/cockroach/issues/57098 22 | del ops['overlaps_above'] # |&> 23 | del ops['overlaps_below'] # &<| 24 | del ops['overlaps_left'] # &< 25 | del ops['overlaps_right'] # &> 26 | # https://github.com/cockroachdb/cockroach/issues/57095 27 | del ops['strictly_above'] # |>> 28 | del ops['strictly_below'] # <<| 29 | return ops 30 | 31 | unsupported_functions = { 32 | 'AsGML', # st_asgml(): https://github.com/cockroachdb/cockroach/issues/48877 33 | 'AsKML', # st_askml(geometry, int): https://github.com/cockroachdb/cockroach/issues/48881 34 | 'AsSVG', # st_assvg(): # https://github.com/cockroachdb/cockroach/issues/48883 35 | 'GeometryDistance', # <-> operator: https://github.com/cockroachdb/cockroach/issues/57099 36 | } 37 | -------------------------------------------------------------------------------- /django_cockroachdb_gis/schema.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor 2 | 3 | from django_cockroachdb.schema import ( 4 | DatabaseSchemaEditor as CockroachSchemaEditor, 5 | ) 6 | 7 | 8 | class DatabaseSchemaEditor(PostGISSchemaEditor, CockroachSchemaEditor): 9 | pass 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-cockroachdb 3 | version = attr: django_cockroachdb.__version__ 4 | url = https://github.com/cockroachdb/django-cockroachdb 5 | maintainer = Cockroach Labs 6 | maintainer_email = python@cockroachlabs.com 7 | license = Apache Software License 8 | description = Django backend for CockroachDB 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Framework :: Django 14 | Framework :: Django :: 5.2 15 | License :: OSI Approved :: Apache Software License 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | Programming Language :: Python :: 3.12 22 | Programming Language :: Python :: 3.13 23 | project_urls = 24 | Source = https://github.com/cockroachdb/django-cockroachdb 25 | Tracker = https://github.com/cockroachdb/django-cockroachdb/issues 26 | 27 | [options] 28 | python_requires = >=3.10 29 | packages = find: 30 | install_requires = 31 | django >= 5.2, < 6.0 32 | 33 | [flake8] 34 | max-line-length = 119 35 | 36 | [isort] 37 | combine_as_imports = true 38 | default_section = THIRDPARTY 39 | include_trailing_comma = true 40 | line_length = 79 41 | multi_line_output = 5 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | --------------------------------------------------------------------------------