Skip to content

Commit 7968f27

Browse files
authored
Merge pull request #17 from portfoliome/uuid-ext
Uuid ext
2 parents 19dfa63 + da2ff6e commit 7968f27

File tree

11 files changed

+193
-15
lines changed

11 files changed

+193
-15
lines changed

postpy/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version_info = (0, 0, 2)
1+
version_info = (0, 0, 3)
22

33
__version__ = '.'.join(map(str, version_info))

postpy/admin.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from postpy.base import Table, Column, Database, PrimaryKey
88
from postpy.ddl import compile_qualified_name
9+
from postpy.extensions import install_extension
910
from postpy.sql import select_dict
1011

1112

@@ -69,12 +70,6 @@ def reflect_table(conn, table_name, schema='public'):
6970
return Table(table_name, columns, primary_key, schema=schema)
7071

7172

72-
def pg_stats_extension_statement():
73-
"""postgres stats extensions statement."""
74-
75-
return 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;'
76-
77-
7873
def reset(db_name):
7974
"""Reset database."""
8075

@@ -87,3 +82,20 @@ def reset(db_name):
8782
cursor.execute(db.create_statement())
8883
conn.close()
8984

85+
86+
def install_extensions(extensions, **connection_parameters):
87+
"""Install Postgres extension if available.
88+
89+
Notes
90+
-----
91+
- superuser is generally required for installing extensions.
92+
- Currently does not support specific schema.
93+
"""
94+
95+
from postpy.connections import connect
96+
97+
conn = connect(**connection_parameters)
98+
conn.autocommit = True
99+
100+
for extension in extensions:
101+
install_extension(conn, extension)

postpy/base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from collections import namedtuple
22

33
from foil.formatters import format_repr
4-
from foil.filters import create_indexer
54

65
from postpy.ddl import (
76
compile_column, compile_qualified_name, compile_primary_key,
@@ -160,9 +159,8 @@ def order_table_columns(table: Table, column_names: list) -> Table:
160159
"""Record table column(s) and primary key columns by specified order."""
161160

162161
unordered_columns = table.column_names
163-
index_order = [unordered_columns.index(name) for name in column_names]
164-
indexer = create_indexer(index_order)
165-
ordered_columns = indexer(table.columns)
162+
index_order = (unordered_columns.index(name) for name in column_names)
163+
ordered_columns = [table.columns[i] for i in index_order]
166164
ordered_pkey_names = [column for column in column_names
167165
if column in table.primary_key_columns]
168166
primary_key = PrimaryKey(ordered_pkey_names)

postpy/extensions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import psycopg2
2+
from psycopg2._psycopg import AsIs
3+
4+
5+
def install_extension(conn, extension: str):
6+
"""Install Postgres extension."""
7+
8+
query = 'CREATE EXTENSION IF NOT EXISTS %s;'
9+
10+
with conn.cursor() as cursor:
11+
cursor.execute(query, (AsIs(extension),))
12+
13+
installed = check_extension(conn, extension)
14+
15+
if not installed:
16+
raise psycopg2.ProgrammingError(
17+
'Postgres extension failed installation.', extension
18+
)
19+
20+
21+
def check_extension(conn, extension: str) -> bool:
22+
"""Check to see if an extension is installed."""
23+
24+
query = 'SELECT installed_version FROM pg_available_extensions WHERE name=%s;'
25+
26+
with conn.cursor() as cursor:
27+
cursor.execute(query, (extension,))
28+
result = cursor.fetchone()
29+
30+
if result is None:
31+
raise psycopg2.ProgrammingError(
32+
'Extension is not available for installation.', extension
33+
)
34+
else:
35+
extension_version = result[0]
36+
37+
return True if extension_version else False

postpy/sql.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ def select_dict(conn, query: str, params=None, name=None, itersize=5000):
7878
yield result
7979

8080

81-
def select_each(conn, query: str, parameter_groups):
81+
def select_each(conn, query: str, parameter_groups, name=None):
8282
"""Run select query for each parameter set in single transaction."""
8383

8484
with conn:
85-
with conn.cursor() as cursor:
85+
with conn.cursor(name=name) as cursor:
8686
for parameters in parameter_groups:
8787
cursor.execute(query, parameters)
8888
yield cursor.fetchone()

postpy/uuids.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Configure psycopg2 to support UUID conversion."""
2+
3+
import psycopg2.extras
4+
5+
from postpy.admin import install_extensions
6+
7+
8+
CRYPTO_EXTENSION = 'pgcrypto'
9+
UUID_OSSP_EXTENSION = 'uuid-ossp'
10+
11+
12+
def register_client():
13+
"""Have psycopg2 marshall UUID objects automatically."""
14+
15+
psycopg2.extras.register_uuid()
16+
17+
18+
def register_crypto():
19+
"""Support for UUID's on server side.
20+
21+
Lighter dependency than uuid-ossp supporting
22+
random_uuid_function for UUID generation.
23+
"""
24+
25+
install_extensions([CRYPTO_EXTENSION])
26+
27+
28+
def register_uuid():
29+
"""Support for UUID's on server side.
30+
31+
Notes
32+
-----
33+
uuid-ossp can be problematic on some platforms. See:
34+
https://www.postgresql.org/docs/current/static/uuid-ossp.html
35+
"""
36+
37+
install_extensions([UUID_OSSP_EXTENSION])
38+
39+
40+
def random_uuid_function(schema=None):
41+
"""Cryptographic random UUID function.
42+
43+
Generates random database side UUID's.
44+
45+
Notes
46+
-----
47+
Lighter dependency than uuid-ossp, but higher
48+
fragmentation on disk if used as auto-generating primary key UUID.
49+
"""
50+
51+
return '{}gen_random_uuid()'.format(_format_schema(schema))
52+
53+
54+
def uuid_sequence_function(schema=None):
55+
"""Sequential UUID generation.
56+
57+
Sequential UUID creation on database side offering
58+
less table fragmentation issues when used as UUID primary key.
59+
"""
60+
61+
return '{}uuid_generate_v1mc()'.format(_format_schema(schema))
62+
63+
64+
def _format_schema(schema):
65+
return '{}.'.format(schema) if schema else ''

tests/test_admin.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import unittest
22

33
from postpy.admin import (get_user_tables, get_primary_keys,
4-
get_column_metadata, reflect_table, reset)
4+
get_column_metadata, install_extensions,
5+
reflect_table, reset)
56
from postpy.base import Database, Column, PrimaryKey, Table
67
from postpy.connections import connect
78
from postpy.fixtures import PostgreSQLFixture
@@ -95,3 +96,21 @@ def tearDown(self):
9596
cursor.execute(self.db.drop_statement())
9697

9798
self.conn.close()
99+
100+
101+
class TestExtensions(PostgreSQLFixture, unittest.TestCase):
102+
@classmethod
103+
def _prep(cls):
104+
cls.pg_extension = 'sslinfo'
105+
cls.conn.autocommit = True
106+
107+
def test_install_extensions(self):
108+
109+
install_extensions([self.pg_extension])
110+
111+
@classmethod
112+
def _clean(cls):
113+
statement = 'DROP EXTENSION IF EXISTS {};'.format(cls.pg_extension)
114+
115+
with cls.conn.cursor() as cursor:
116+
cursor.execute(statement)

tests/test_base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,14 @@ def test_make_delete_table(self):
230230

231231
class TestReOrderTableColumns(unittest.TestCase):
232232
def setUp(self):
233+
self.maxDiff = None
233234
self.schema = 'foo'
234235
self.table_name = 'foobar'
235236
columns = table_columns()
236237
primary_key = table_primary_keys()
237238
self.table = Table(self.table_name, columns, primary_key, self.schema)
238239

239-
def reorder_columns(self):
240+
def test_reorder_columns(self):
240241
column_names = ['state', 'city', 'population']
241242

242243
expect_columns = [Column('state', 'CHAR(2)', False),

tests/test_extensions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import unittest
2+
3+
import psycopg2
4+
5+
from postpy.fixtures import PostgreSQLFixture
6+
from postpy.extensions import check_extension
7+
8+
9+
class TestExtensions(PostgreSQLFixture, unittest.TestCase):
10+
@classmethod
11+
def _prep(cls):
12+
cls.pg_extension = 'sslinfo'
13+
14+
def test_check_no_extension(self):
15+
with self.assertRaises(psycopg2.ProgrammingError):
16+
check_extension(self.conn, 'fake_extension')
17+
18+
def test_check_uninstalled_extension(self):
19+
20+
self.assertFalse(check_extension(self.conn, self.pg_extension))

tests/test_sql.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,11 @@ def test_query_columns(self):
9999
result = list(sql.query_columns(self.conn, query))
100100

101101
self.assertEqual(expected, result)
102+
query = 'select * from generate_series(1,3) as col1 where col1=%s;'
103+
parameter_groups = [(3,), (2,), (1,)]
104+
Record = namedtuple('Record', 'col1')
105+
106+
expected = [Record(col1=3), Record(col1=2), Record(col1=1)]
107+
result = list(sql.select_each(self.conn, query, parameter_groups))
108+
109+
self.assertEqual(expected, result)

0 commit comments

Comments
 (0)