├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── UNLICENSE ├── src └── uuid.c ├── test ├── conftest.py └── test_uuid.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.2.0 3 | commit = True 4 | tag = True 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | .pytest_cache/ 4 | .tox/ 5 | __pycache__ 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | 5 | matrix: 6 | include: 7 | - os: linux 8 | - os: osx 9 | language: generic 10 | # This is a hack to set up tox-travis on OS X, since this is a generic 11 | # build. Travis sets this automatically for Python builds. 12 | env: TRAVIS_PYTHON_VERSION=3.6 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - gcc 18 | - libssl-dev 19 | - libsqlite3-dev 20 | - uuid-dev 21 | 22 | before_install: | 23 | if [[ $TRAVIS_OS_NAME == 'osx' ]]; then 24 | pyenv install 3.6.5 25 | pyenv global 3.6.5 26 | fi 27 | 28 | install: 29 | - pip install tox-travis 30 | 31 | script: 32 | - make DEBUG=1 33 | - make test USE_DEFAULT_ENTRY_POINT=1 34 | 35 | after_success: | 36 | if [[ $TRAVIS_OS_NAME == 'linux' ]]; then 37 | gcov src/uuid.c 38 | bash <(curl -s https://codecov.io/bash) 39 | fi 40 | 41 | notifications: 42 | email: false 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean test 2 | 3 | NAME = uuid 4 | VERSION := $(shell git describe --tags --always --dirty --match v* | sed 's/^v//') 5 | PLATFORM := $(shell uname -s) 6 | CFLAGS = -g -fPIC -Wall -Wextra -Wno-unused-parameter -O2 -lcrypto 7 | LDFLAGS = -shared 8 | DIST_DIR = dist 9 | SOURCES := $(wildcard src/*.c) 10 | 11 | ifeq ($(PLATFORM),Darwin) 12 | SO_EXT = dylib 13 | USE_COMMONCRYPTO = implicit 14 | LIB := $(DIST_DIR)/$(NAME).$(VERSION).$(SO_EXT) 15 | else 16 | SO_EXT = so 17 | USE_OPENSSL = implicit 18 | LDFLAGS += -luuid 19 | LIB := $(DIST_DIR)/$(NAME).$(SO_EXT).$(VERSION) 20 | endif 21 | 22 | ifneq ($(USE_OPENSSL),) 23 | CFLAGS += -DUSE_OPENSSL 24 | endif 25 | 26 | ifneq ($(USE_COMMONCRYPTO),) 27 | CFLAGS += -DUSE_COMMONCRYPTO 28 | endif 29 | 30 | ifneq ($(USE_DEFAULT_ENTRY_POINT),) 31 | CFLAGS += -DUSE_DEFAULT_ENTRY_POINT 32 | endif 33 | 34 | ifneq ($(DEBUG),) 35 | CFLAGS += --coverage 36 | endif 37 | 38 | all: $(LIB) 39 | 40 | clean: 41 | $(RM) -r $(DIST_DIR) 42 | $(RM) *.gcda *.gcno *.gcov 43 | 44 | print-%: 45 | @echo '$*=$($*)' 46 | 47 | test: $(LIB) 48 | tox -- --extension=$(LIB) 49 | 50 | $(LIB): $(SOURCES) 51 | mkdir -p $(DIST_DIR) 52 | $(CC) $< $(CFLAGS) $(LDFLAGS) -o $@ 53 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | tox = "*" 10 | 11 | [requires] 12 | python_version = "3.6" 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5d37ba76d9b94b13859453afd23913177971174579be6d6a7410dce320a404ed" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "pluggy": { 21 | "hashes": [ 22 | "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", 23 | "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" 24 | ], 25 | "version": "==0.7.1" 26 | }, 27 | "py": { 28 | "hashes": [ 29 | "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", 30 | "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" 31 | ], 32 | "version": "==1.5.4" 33 | }, 34 | "six": { 35 | "hashes": [ 36 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 37 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 38 | ], 39 | "version": "==1.11.0" 40 | }, 41 | "tox": { 42 | "hashes": [ 43 | "sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7", 44 | "sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600" 45 | ], 46 | "index": "pypi", 47 | "version": "==3.2.1" 48 | }, 49 | "virtualenv": { 50 | "hashes": [ 51 | "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", 52 | "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" 53 | ], 54 | "version": "==16.0.0" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlite3-uuid 2 | 3 | [![Build Status](https://travis-ci.org/benwebber/sqlite3-uuid.svg?branch=master)](https://travis-ci.org/benwebber/sqlite3-uuid) 4 | [![codecov](https://codecov.io/gh/benwebber/sqlite3-uuid/branch/master/graph/badge.svg)](https://codecov.io/gh/benwebber/sqlite3-uuid) 5 | 6 | This SQLite extension implements functions for creating [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt) compliant UUIDs. 7 | 8 | ## Installation 9 | 10 | 1. Install the necessary development headers. Mac OS X ships with SQLite and the development headers, but you may need to install them on Linux. 11 | 12 | * Debian / Ubuntu: 13 | 14 | ``` 15 | sudo apt-get install libsqlite3-dev libssl-dev uuid-dev 16 | ``` 17 | 18 | * Red Hat / CentOS: 19 | 20 | ``` 21 | sudo yum install openssl-devel sqlite-devel uuid-devel 22 | ``` 23 | 24 | 2. Build the extension. The build will produce a shared library for your platform under `dist/`. 25 | 26 | ``` 27 | make 28 | ``` 29 | 30 | 2. Load the extension using your SQLite API of choice. For example, in Python, 31 | 32 | ```python 33 | import sqlite3 34 | 35 | con = sqlite3.connect(':memory:') 36 | 37 | con.enable_load_extension(True) 38 | con.load_extension('/path/to/uuid') 39 | con.enable_load_extension(False) 40 | 41 | for row in con.execute('SELECT uuid4();'): 42 | print(row) 43 | ``` 44 | 45 | ## Usage 46 | 47 | ### `uuid1()` 48 | 49 | Generate a version 1 UUID, based on the time and host machine's MAC address. 50 | 51 | ```sql 52 | SELECT uuid1(); 53 | d5a80b20-0d8f-11e5-b8cb-080027b6ec40 54 | ``` 55 | 56 | ### `uuid3()` 57 | 58 | Generate a version 3 (MD5) namespace UUID. 59 | 60 | ``` 61 | SELECT uuid3(uuid_ns_dns(), 'example.org'); 62 | 04738bdf-b25a-3829-a801-b21a1d25095b 63 | ``` 64 | 65 | ### `uuid4()` 66 | 67 | Generate a version 4 (random) UUID. 68 | 69 | ```sql 70 | SELECT uuid4(); 71 | 1e607604-f360-4fa5-863a-bc91adc70bb9 72 | ``` 73 | 74 | ### `uuid5()` 75 | 76 | Generate a version 5 (SHA1) namespace UUID. 77 | 78 | ``` 79 | SELECT uuid3(uuid_ns_dns(), 'example.org'); 80 | aad03681-8b63-5304-89e0-8ca8f49461b5 81 | ``` 82 | 83 | ### `uuid_nil()` 84 | 85 | Generate the nil UUID. 86 | 87 | ``` 88 | SELECT uuid_nil(); 89 | 00000000-0000-0000-0000-000000000000 90 | ``` 91 | 92 | ### Namespaces 93 | 94 | #### DNS 95 | 96 | ``` 97 | SELECT uuid_ns_dns(); 98 | 6ba7b810-9dad-11d1-80b4-00c04fd430c8 99 | ``` 100 | 101 | #### OID 102 | 103 | ``` 104 | SELECT uuid_ns_oid(); 105 | 6ba7b812-9dad-11d1-80b4-00c04fd430c8 106 | ``` 107 | 108 | #### URL 109 | 110 | ``` 111 | SELECT uuid_ns_url(); 112 | 6ba7b811-9dad-11d1-80b4-00c04fd430c8 113 | ``` 114 | 115 | #### X500 116 | 117 | ``` 118 | SELECT uuid_ns_x500(); 119 | 6ba7b814-9dad-11d1-80b4-00c04fd430c8 120 | ``` 121 | 122 | ### Converting between types 123 | 124 | #### `uuid_to_blob()` 125 | 126 | Convert a TEXT UUID to a 16-byte BLOB. 127 | 128 | ``` 129 | sqlite3 -cmd '.load uuid' uuid.db 'SELECT uuid_to_blob("3dfe5c62-e543-46ff-a2e0-0b1017506be0");' | 130 | tr -d '\n' | 131 | hexdump -C 132 | 00000000 3d fe 5c 62 e5 43 46 ff a2 e0 0b 10 17 50 6b e0 |=.\b.CF......Pk.| 133 | 00000010 134 | ``` 135 | 136 | ### `uuid_to_text()` 137 | 138 | Convert a 16-byte BLOB to a TEXT UUID. 139 | 140 | ``` 141 | SELECT uuid_to_text(uuid_to_blob('3dfe5c62-e543-46ff-a2e0-0b1017506be0')); 142 | 3dfe5c62-e543-46ff-a2e0-0b1017506be0 143 | ``` 144 | 145 | ## Notes 146 | 147 | This extension only supports Linux and Mac OS X at the moment. 148 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/uuid.c: -------------------------------------------------------------------------------- 1 | /* 2 | ** The author disclaims copyright to this source code. In place of 3 | ** a legal notice, here is a blessing: 4 | ** 5 | ** May you do good and not evil. 6 | ** May you find forgiveness for yourself and forgive others. 7 | ** May you share freely, never taking more than you give. 8 | ** 9 | ************************************************************************* 10 | ** 11 | ** This SQLite extension implements functions for creating RFC 4122 compliant 12 | ** UUIDs. 13 | ** 14 | */ 15 | #include 16 | SQLITE_EXTENSION_INIT1 17 | #include 18 | #include 19 | #include 20 | 21 | #ifdef USE_OPENSSL 22 | #include 23 | #include 24 | #endif 25 | 26 | #ifdef USE_COMMONCRYPTO 27 | #define COMMON_DIGEST_FOR_OPENSSL 28 | #include 29 | #endif 30 | 31 | #ifdef USE_DEFAULT_ENTRY_POINT 32 | #define ENTRY_POINT sqlite3_extension_init 33 | #else 34 | #define ENTRY_POINT sqlite3_uuid_init 35 | #endif 36 | 37 | #define SET_VARIANT(uu) (uu[8] = (uu[8] & 0xbf) | 0x80) 38 | #define SET_VERSION(uu, version) (uu[6] = (uu[6] & 0x0f) | (version << 4)) 39 | 40 | #define UUID_LENGTH 36 41 | 42 | typedef char _uuid_string_t[37]; 43 | 44 | static const _uuid_string_t NAMESPACE_DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; 45 | static const _uuid_string_t NAMESPACE_OID = "6ba7b812-9dad-11d1-80b4-00c04fd430c8"; 46 | static const _uuid_string_t NAMESPACE_URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8"; 47 | static const _uuid_string_t NAMESPACE_X500 = "6ba7b814-9dad-11d1-80b4-00c04fd430c8"; 48 | static const _uuid_string_t NIL_UUID = "00000000-0000-0000-0000-000000000000"; 49 | 50 | /* 51 | ** Implementation of uuid1() function. 52 | */ 53 | static void uuid1( 54 | sqlite3_context *context, 55 | int argc, 56 | sqlite3_value **argv 57 | ){ 58 | assert(argc==0); 59 | uuid_t uuid; 60 | uuid_generate_time(uuid); 61 | _uuid_string_t uuid_str; 62 | uuid_unparse_lower(uuid, uuid_str); 63 | sqlite3_result_text(context, uuid_str, UUID_LENGTH, SQLITE_TRANSIENT); 64 | } 65 | 66 | /* 67 | ** Implementation of the uuid3() function. 68 | */ 69 | static void uuid3( 70 | sqlite3_context *context, 71 | int argc, 72 | sqlite3_value **argv 73 | ){ 74 | assert(argc==2); 75 | uuid_t namespace_uuid; 76 | _uuid_string_t uuid_str; 77 | 78 | const unsigned char *namespace = sqlite3_value_text(argv[0]); 79 | const unsigned char *name = sqlite3_value_text(argv[1]); 80 | 81 | if(uuid_parse((const char *)namespace, namespace_uuid) == -1) { 82 | sqlite3_result_error(context, "cannot parse namespace UUID", -1); 83 | return; 84 | } 85 | 86 | uuid_t uu; 87 | 88 | MD5_CTX mdctx; 89 | unsigned char md_value[MD5_DIGEST_LENGTH]; 90 | MD5_Init(&mdctx); 91 | MD5_Update(&mdctx, namespace_uuid, sizeof(uuid_t)); 92 | MD5_Update(&mdctx, name, strlen((const char *)name)); 93 | MD5_Final(md_value, &mdctx); 94 | 95 | SET_VARIANT(md_value); 96 | SET_VERSION(md_value, 3); 97 | 98 | memcpy(uu, md_value, sizeof(uuid_t)); 99 | 100 | uuid_unparse_lower(uu, uuid_str); 101 | sqlite3_result_text(context, uuid_str, UUID_LENGTH, SQLITE_TRANSIENT); 102 | } 103 | 104 | /* 105 | ** Implementation of the uuid4() function. 106 | */ 107 | static void uuid4( 108 | sqlite3_context *context, 109 | int argc, 110 | sqlite3_value **argv 111 | ){ 112 | assert(argc==0); 113 | uuid_t uuid; 114 | uuid_generate_random(uuid); 115 | _uuid_string_t uuid_str; 116 | uuid_unparse_lower(uuid, uuid_str); 117 | sqlite3_result_text(context, uuid_str, UUID_LENGTH, SQLITE_TRANSIENT); 118 | } 119 | 120 | /* 121 | ** Implementation of the uuid5() function. 122 | */ 123 | static void uuid5( 124 | sqlite3_context *context, 125 | int argc, 126 | sqlite3_value **argv 127 | ){ 128 | assert(argc==2); 129 | uuid_t namespace_uuid; 130 | _uuid_string_t uuid_str; 131 | 132 | const unsigned char *namespace = sqlite3_value_text(argv[0]); 133 | const unsigned char *name = sqlite3_value_text(argv[1]); 134 | 135 | if(uuid_parse((const char *)namespace, namespace_uuid) == -1) { 136 | sqlite3_result_error(context, "cannot parse namespace UUID", -1); 137 | return; 138 | } 139 | 140 | uuid_t uu; 141 | 142 | SHA_CTX mdctx; 143 | unsigned char md_value[SHA_DIGEST_LENGTH]; 144 | SHA1_Init(&mdctx); 145 | SHA1_Update(&mdctx, namespace_uuid, sizeof(uuid_t)); 146 | SHA1_Update(&mdctx, name, strlen((const char *)name)); 147 | SHA1_Final(md_value, &mdctx); 148 | 149 | SET_VARIANT(md_value); 150 | SET_VERSION(md_value, 5); 151 | 152 | memcpy(uu, md_value, sizeof(uuid_t)); 153 | 154 | uuid_unparse_lower(uu, uuid_str); 155 | sqlite3_result_text(context, uuid_str, UUID_LENGTH, SQLITE_TRANSIENT); 156 | } 157 | 158 | static void uuid_nil( 159 | sqlite3_context *context, 160 | int argc, 161 | sqlite3_value **argv 162 | ){ 163 | assert(argc==0); 164 | sqlite3_result_text(context, NIL_UUID, UUID_LENGTH, SQLITE_STATIC); 165 | } 166 | 167 | static void uuid_ns_dns( 168 | sqlite3_context *context, 169 | int argc, 170 | sqlite3_value **argv 171 | ){ 172 | assert(argc==0); 173 | sqlite3_result_text(context, NAMESPACE_DNS, UUID_LENGTH, SQLITE_STATIC); 174 | } 175 | 176 | static void uuid_ns_oid( 177 | sqlite3_context *context, 178 | int argc, 179 | sqlite3_value **argv 180 | ){ 181 | assert(argc==0); 182 | sqlite3_result_text(context, NAMESPACE_OID, UUID_LENGTH, SQLITE_STATIC); 183 | } 184 | 185 | static void uuid_ns_url( 186 | sqlite3_context *context, 187 | int argc, 188 | sqlite3_value **argv 189 | ){ 190 | assert(argc==0); 191 | sqlite3_result_text(context, NAMESPACE_URL, UUID_LENGTH, SQLITE_STATIC); 192 | } 193 | 194 | static void uuid_ns_x500( 195 | sqlite3_context *context, 196 | int argc, 197 | sqlite3_value **argv 198 | ){ 199 | assert(argc==0); 200 | sqlite3_result_text(context, NAMESPACE_X500, UUID_LENGTH, SQLITE_STATIC); 201 | } 202 | 203 | static void uuid_to_text( 204 | sqlite3_context *context, 205 | int argc, 206 | sqlite3_value **argv 207 | ){ 208 | assert(argc==1); 209 | const void *uuid_bytes = sqlite3_value_blob(argv[0]); 210 | if(sqlite3_value_bytes(argv[0]) != sizeof(uuid_t)) { 211 | sqlite3_result_error(context, "UUID must be 16 bytes", -1); 212 | return; 213 | } 214 | _uuid_string_t uuid_str; 215 | uuid_unparse_lower(*(uuid_t *)uuid_bytes, uuid_str); 216 | sqlite3_result_text(context, uuid_str, UUID_LENGTH, SQLITE_TRANSIENT); 217 | } 218 | 219 | static void uuid_to_blob( 220 | sqlite3_context *context, 221 | int argc, 222 | sqlite3_value **argv 223 | ){ 224 | assert(argc==1); 225 | uuid_t uuid; 226 | const unsigned char *uuid_str = sqlite3_value_text(argv[0]); 227 | if(uuid_parse((const char *)uuid_str, uuid) == -1) { 228 | sqlite3_result_error(context, "cannot parse UUID", -1); 229 | return; 230 | } 231 | sqlite3_result_blob(context, uuid, sizeof(uuid_t), SQLITE_TRANSIENT); 232 | } 233 | 234 | /* 235 | ** Register UUID functions to database `db`. 236 | */ 237 | static int register_uuid_functions(sqlite3 *db) { 238 | typedef struct UUIDScalar { 239 | const char *name; 240 | int argc; 241 | int enc; 242 | void (*func)(sqlite3_context*, int, sqlite3_value**); 243 | } UUIDScalar; 244 | 245 | UUIDScalar scalars[] = { 246 | {"uuid1", 0, SQLITE_UTF8, uuid1}, 247 | {"uuid3", 2, SQLITE_UTF8, uuid3}, 248 | {"uuid4", 0, SQLITE_UTF8, uuid4}, 249 | {"uuid5", 2, SQLITE_UTF8, uuid5}, 250 | {"uuid_nil", 0, SQLITE_UTF8, uuid_nil}, 251 | {"uuid_ns_dns", 0, SQLITE_UTF8, uuid_ns_dns}, 252 | {"uuid_ns_oid", 0, SQLITE_UTF8, uuid_ns_oid}, 253 | {"uuid_ns_url", 0, SQLITE_UTF8, uuid_ns_url}, 254 | {"uuid_ns_x500", 0, SQLITE_UTF8, uuid_ns_x500}, 255 | {"uuid_to_text", 1, SQLITE_UTF8, uuid_to_text}, 256 | {"uuid_to_blob", 1, SQLITE_UTF8, uuid_to_blob}, 257 | }; 258 | 259 | int rc = SQLITE_OK; 260 | int i, n; 261 | 262 | n = (int)(sizeof(scalars)/sizeof(scalars[0])); 263 | 264 | for (i = 0; rc == SQLITE_OK && i < n; i++) { 265 | UUIDScalar *s = &scalars[i]; 266 | rc = sqlite3_create_function(db, s->name, s->argc, s->enc, 0, s->func, 0, 0); 267 | } 268 | 269 | return rc; 270 | } 271 | 272 | #ifdef _WIN32 273 | __declspec(dllexport) 274 | #endif 275 | int ENTRY_POINT( 276 | sqlite3 *db, 277 | char **pzErrMsg, 278 | const sqlite3_api_routines *pApi 279 | ){ 280 | SQLITE_EXTENSION_INIT2(pApi); 281 | return register_uuid_functions(db); 282 | } 283 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os.path 3 | import uuid 4 | from typing import NamedTuple 5 | 6 | import apsw 7 | import pytest 8 | 9 | 10 | class NamespaceExample(NamedTuple): 11 | namespace: uuid.UUID 12 | name: str 13 | 14 | 15 | def pytest_addoption(parser): 16 | parser.addoption( 17 | '--extension', 18 | action='store', 19 | help='path to sqlite3-uuid shared library', 20 | default=os.path.join(os.path.dirname(__file__), '../dist/uuid'), 21 | ) 22 | 23 | 24 | @pytest.fixture 25 | def extension(request): 26 | return request.config.getoption('--extension') 27 | 28 | 29 | @pytest.fixture 30 | def db(extension): 31 | conn = apsw.Connection(':memory:') 32 | conn.enableloadextension(True) 33 | conn.loadextension(extension) 34 | conn.enableloadextension(False) 35 | return conn 36 | 37 | 38 | @pytest.fixture 39 | def examples(): 40 | return [ 41 | NamespaceExample(uuid.NAMESPACE_DNS, 'example.org'), 42 | NamespaceExample(uuid.NAMESPACE_DNS, 'www.example.org'), 43 | NamespaceExample(uuid.NAMESPACE_OID, '0.1.2.3'), 44 | NamespaceExample(uuid.NAMESPACE_OID, '0.1.2.3.4'), 45 | NamespaceExample(uuid.NAMESPACE_URL, 'https://example.org'), 46 | NamespaceExample(uuid.NAMESPACE_URL, 'https://www.example.org'), 47 | NamespaceExample(uuid.NAMESPACE_X500, 'cn=www.example.org,ou=Technology,o=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US'), 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def uuid3_examples(examples): 53 | return [((str(e.namespace), e.name), uuid.uuid3(e.namespace, e.name)) 54 | for e in examples] 55 | 56 | 57 | @pytest.fixture 58 | def uuid5_examples(examples): 59 | return [((str(e.namespace), e.name), uuid.uuid5(e.namespace, e.name)) 60 | for e in examples] 61 | -------------------------------------------------------------------------------- /test/test_uuid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | sqlite-uuid test suite 5 | """ 6 | 7 | import secrets 8 | import uuid 9 | 10 | import apsw 11 | import pytest 12 | 13 | 14 | def query(db, *args): 15 | with db: 16 | return db.cursor().execute(*args).fetchone()[0] 17 | 18 | 19 | class TestUUIDv3: 20 | def test_uuid3(self, db, uuid3_examples): 21 | """ 22 | Should return expected v3 UUID for known input. 23 | """ 24 | for example in uuid3_examples: 25 | args, expected = example 26 | result = query(db, 'SELECT uuid3(?, ?);', args) 27 | u = uuid.UUID(result) 28 | assert u == expected 29 | assert u.version == 3 30 | assert u.variant == uuid.RFC_4122 31 | 32 | def test_uuid3_invalid_namespace(self, db): 33 | """ 34 | Should raise an error for invalid or namespace UUID. 35 | """ 36 | with pytest.raises(apsw.SQLError) as exc: 37 | query(db, 'SELECT uuid3(?, ?);', ('this is not a UUID', 'test')) 38 | assert 'cannot parse namespace UUID' in str(exc) 39 | 40 | 41 | class TestUUIDv5: 42 | def test_uuid5(self, db, uuid5_examples): 43 | """ 44 | Should return expected v5 UUID for known input. 45 | """ 46 | for example in uuid5_examples: 47 | args, expected = example 48 | result = query(db, 'SELECT uuid5(?, ?);', args) 49 | u = uuid.UUID(result) 50 | assert u == expected 51 | assert u.version == 5 52 | assert u.variant == uuid.RFC_4122 53 | 54 | def test_uuid5_invalid_namespace(self, db): 55 | """ 56 | Should raise an error for invalid or namespace UUID. 57 | """ 58 | with pytest.raises(apsw.SQLError) as exc: 59 | query(db, 'SELECT uuid5(?, ?);', ('this is not a UUID', 'test')) 60 | assert 'cannot parse namespace UUID' in str(exc) 61 | 62 | class TestUUIDConversion: 63 | def test_uuid_to_text(self, db, uuid5_examples): 64 | """ 65 | Should convert binary UUID (bytes) to canonical string representation. 66 | """ 67 | for _, expected in uuid5_examples: 68 | result = query(db, 'SELECT uuid_to_text(?);', (expected.bytes,)) 69 | u = uuid.UUID(result) 70 | assert u == expected 71 | 72 | def test_uuid_to_text_invalid_length(self, db, uuid5_examples): 73 | """ 74 | Should raise an error if binary UUID data is not 16 bytes long. 75 | """ 76 | with pytest.raises(apsw.SQLError) as exc: 77 | buf = secrets.token_bytes(15) 78 | query(db, 'SELECT uuid_to_text(?);', (buf,)) 79 | assert 'UUID must be 16 bytes' in str(exc) 80 | 81 | def test_uuid_to_blob(self, db, uuid5_examples): 82 | """ 83 | Should convert canonical string UUID to binary (bytes). 84 | """ 85 | for _, expected in uuid5_examples: 86 | result = query(db, 'SELECT uuid_to_blob(?);', (str(expected),)) 87 | u = uuid.UUID(bytes=result) 88 | assert u == expected 89 | 90 | def test_uuid_to_blob_invalid_uuid(self, db): 91 | """ 92 | Should raise an error if string is not UUID. 93 | """ 94 | with pytest.raises(apsw.SQLError) as exc: 95 | query(db, 'SELECT uuid_to_blob(?);', ('foo',)) 96 | assert 'cannot parse UUID' in str(exc) 97 | 98 | 99 | def test_uuid1(db): 100 | result = query(db, 'SELECT uuid1();') 101 | u = uuid.UUID(result) 102 | assert u.version == 1 103 | assert u.variant == uuid.RFC_4122 104 | 105 | 106 | def test_uuid4(db): 107 | result = query(db, 'SELECT uuid4();') 108 | u = uuid.UUID(result) 109 | assert u.version == 4 110 | assert u.variant == uuid.RFC_4122 111 | 112 | 113 | def test_uuid_nil(db): 114 | result = query(db, 'SELECT uuid_nil();') 115 | u = uuid.UUID(result) 116 | assert u == uuid.UUID('00000000-0000-0000-0000-000000000000') 117 | assert u.variant == uuid.RESERVED_NCS 118 | 119 | 120 | @pytest.mark.parametrize('sql, expected', [ 121 | ('SELECT uuid_ns_dns();', uuid.NAMESPACE_DNS), 122 | ('SELECT uuid_ns_oid();', uuid.NAMESPACE_OID), 123 | ('SELECT uuid_ns_url();', uuid.NAMESPACE_URL), 124 | ('SELECT uuid_ns_x500();', uuid.NAMESPACE_X500), 125 | ]) 126 | def test_namespace_uuids(db, sql, expected): 127 | result = query(db, sql) 128 | assert uuid.UUID(result) == expected 129 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = py36 4 | 5 | [testenv] 6 | deps = 7 | apsw 8 | pytest 9 | commands = 10 | pytest --verbose {posargs} 11 | --------------------------------------------------------------------------------