DefPixel-PostgreSQL 0.1.5__83-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/Array.py +16 -0
  2. DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/JavaScriptObjectNotation.py +74 -0
  3. DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/__init__.py +51 -0
  4. DefPixel/DataAccessObject/Implements/PostgreSQL/Column.py +34 -0
  5. DefPixel/DataAccessObject/Implements/PostgreSQL/Compiler.py +161 -0
  6. DefPixel/DataAccessObject/Implements/PostgreSQL/Configuration.py +116 -0
  7. DefPixel/DataAccessObject/Implements/PostgreSQL/Connection.py +138 -0
  8. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraint.py +18 -0
  9. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Check.py +23 -0
  10. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Columns.py +24 -0
  11. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/ForeignKey.py +56 -0
  12. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/PrimaryKey.py +21 -0
  13. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Unique.py +25 -0
  14. DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/__init__.py +13 -0
  15. DefPixel/DataAccessObject/Implements/PostgreSQL/Context.py +48 -0
  16. DefPixel/DataAccessObject/Implements/PostgreSQL/Cursor.py +123 -0
  17. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Create.py +181 -0
  18. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Delete.py +31 -0
  19. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Drop.py +53 -0
  20. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Get.py +24 -0
  21. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Insert.py +98 -0
  22. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Query.py +37 -0
  23. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Select.py +122 -0
  24. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Update.py +57 -0
  25. DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/__init__.py +19 -0
  26. DefPixel/DataAccessObject/Implements/PostgreSQL/Expr.py +11 -0
  27. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Common.py +180 -0
  28. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Compare.py +87 -0
  29. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Condition.py +155 -0
  30. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/JavaScriptObjectNotation.py +53 -0
  31. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Operator.py +42 -0
  32. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/String.py +48 -0
  33. DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/__init__.py +75 -0
  34. DefPixel/DataAccessObject/Implements/PostgreSQL/Function.py +11 -0
  35. DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Common.py +142 -0
  36. DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/DateTime.py +219 -0
  37. DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/JavaScriptObjectNotation.py +100 -0
  38. DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Sequence.py +55 -0
  39. DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/__init__.py +91 -0
  40. DefPixel/DataAccessObject/Implements/PostgreSQL/Index.py +135 -0
  41. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/FullOuter.py +15 -0
  42. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Inner.py +15 -0
  43. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Join.py +41 -0
  44. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/LeftOuter.py +15 -0
  45. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/RightOuter.py +15 -0
  46. DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/__init__.py +15 -0
  47. DefPixel/DataAccessObject/Implements/PostgreSQL/Mapping.py +359 -0
  48. DefPixel/DataAccessObject/Implements/PostgreSQL/Orders.py +59 -0
  49. DefPixel/DataAccessObject/Implements/PostgreSQL/Pool.py +84 -0
  50. DefPixel/DataAccessObject/Implements/PostgreSQL/Schema.py +28 -0
  51. DefPixel/DataAccessObject/Implements/PostgreSQL/Sequence.py +55 -0
  52. DefPixel/DataAccessObject/Implements/PostgreSQL/Session.py +84 -0
  53. DefPixel/DataAccessObject/Implements/PostgreSQL/Type.py +113 -0
  54. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Array.py +61 -0
  55. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Binary.py +34 -0
  56. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Bool.py +29 -0
  57. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/DateTime.py +120 -0
  58. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/JavaScriptObjectNotation.py +52 -0
  59. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Numeric.py +151 -0
  60. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Size.py +13 -0
  61. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/String.py +80 -0
  62. DefPixel/DataAccessObject/Implements/PostgreSQL/Types/__init__.py +76 -0
  63. DefPixel/DataAccessObject/Implements/PostgreSQL/__init__.py +85 -0
  64. defpixel_postgresql-0.1.5.dist-info/METADATA +180 -0
  65. defpixel_postgresql-0.1.5.dist-info/RECORD +68 -0
  66. defpixel_postgresql-0.1.5.dist-info/WHEEL +6 -0
  67. defpixel_postgresql-0.1.5.dist-info/licenses/LICENSE +674 -0
  68. defpixel_postgresql-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Sequence
4
+
5
+ from psycopg.types.array import ListDumper
6
+
7
+ __all__ = (
8
+ 'ArrayDumper',
9
+ )
10
+
11
+
12
+ class ArrayDumper(ListDumper):
13
+ """Dump tuples as PostgreSQL arrays in the same way as lists."""
14
+
15
+ def dump(self, o: Sequence):
16
+ return super().dump(list(o))
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections.abc import (
4
+ Mapping,
5
+ Set,
6
+ )
7
+ from dataclasses import (
8
+ fields,
9
+ is_dataclass,
10
+ )
11
+ from datetime import (
12
+ date,
13
+ datetime,
14
+ time,
15
+ timedelta,
16
+ )
17
+ from decimal import Decimal
18
+ from json import dumps
19
+ from typing import Any
20
+ from uuid import UUID
21
+
22
+ from psycopg.adapt import Dumper
23
+
24
+ __all__ = (
25
+ 'encode',
26
+ 'JavaScriptObjectNotationDumper',
27
+ 'DataclassDumper',
28
+ )
29
+
30
+
31
+ def encode(o: Any) -> Any:
32
+ """Convert values that the standard JSON encoder cannot serialize."""
33
+ if is_dataclass(o) and not isinstance(o, type):
34
+ return {member.name: getattr(o, member.name) for member in fields(o)}
35
+ if isinstance(o, (datetime, date, time)):
36
+ return o.isoformat()
37
+ if isinstance(o, timedelta):
38
+ return o.total_seconds()
39
+ if isinstance(o, (Decimal, UUID)):
40
+ return str(o)
41
+ if isinstance(o, Set):
42
+ return list(o)
43
+ if isinstance(o, Mapping):
44
+ return dict(o)
45
+ if isinstance(o, tuple):
46
+ return list(o)
47
+ if isinstance(o, (bytes, bytearray, memoryview)):
48
+ raise TypeError('Binary values cannot be encoded as JSON')
49
+ raise TypeError('{} cannot be encoded as JSON'.format(o.__class__.__name__))
50
+
51
+
52
+ def serialize(o: Any) -> bytes:
53
+ return dumps(
54
+ o,
55
+ default=encode,
56
+ ensure_ascii=False,
57
+ allow_nan=False,
58
+ ).encode('utf-8')
59
+
60
+
61
+ class JavaScriptObjectNotationDumper(Dumper):
62
+ """Dump mappings as JSON text and let PostgreSQL infer json or jsonb from the context."""
63
+
64
+ def dump(self, o: Any) -> bytes:
65
+ return serialize(o)
66
+
67
+
68
+ class DataclassDumper(Dumper):
69
+ """Dump dataclass instances as JSON objects; reject other unadaptable objects."""
70
+
71
+ def dump(self, o: Any) -> bytes:
72
+ if not is_dataclass(o) or isinstance(o, type):
73
+ raise TypeError('cannot adapt type {!r}'.format(o.__class__.__name__))
74
+ return serialize(o)
@@ -0,0 +1,51 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import (
4
+ Mapping as TypingMapping,
5
+ Optional,
6
+ )
7
+
8
+ from psycopg.adapt import (
9
+ AdaptersMap,
10
+ Dumper,
11
+ Loader,
12
+ )
13
+
14
+ from .Array import ArrayDumper
15
+ from .JavaScriptObjectNotation import (
16
+ DataclassDumper,
17
+ JavaScriptObjectNotationDumper,
18
+ )
19
+
20
+ __all__ = (
21
+ 'ArrayDumper',
22
+ 'DataclassDumper',
23
+ 'JavaScriptObjectNotationDumper',
24
+ 'DUMPERS',
25
+ 'register',
26
+ )
27
+
28
+ DUMPERS = {
29
+ dict: JavaScriptObjectNotationDumper,
30
+ tuple: ArrayDumper,
31
+ object: DataclassDumper,
32
+ }
33
+
34
+
35
+ def register(
36
+ adapters: AdaptersMap,
37
+ dumpers: Optional[TypingMapping] = None,
38
+ loaders: Optional[TypingMapping] = None,
39
+ ) -> AdaptersMap:
40
+ """Register DefPixel defaults first, then user dumpers and loaders that may override them."""
41
+ for cls, dumper in DUMPERS.items():
42
+ adapters.register_dumper(cls, dumper)
43
+ for cls, dumper in (dumpers or {}).items():
44
+ if not isinstance(dumper, type) or not issubclass(dumper, Dumper):
45
+ raise TypeError('Dumpers must be psycopg Dumper classes')
46
+ adapters.register_dumper(cls, dumper)
47
+ for oid, loader in (loaders or {}).items():
48
+ if not isinstance(loader, type) or not issubclass(loader, Loader):
49
+ raise TypeError('Loaders must be psycopg Loader classes')
50
+ adapters.register_loader(oid, loader)
51
+ return adapters
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from .Compiler import (
6
+ Compilable,
7
+ Compiler,
8
+ identifier,
9
+ typename,
10
+ )
11
+
12
+ __all__ = (
13
+ 'Column',
14
+ )
15
+
16
+
17
+ class Column(Compilable):
18
+ """Unqualified column reference with an optional type cast."""
19
+
20
+ def __init__(self, name: str):
21
+ identifier(name)
22
+ self.name = name
23
+ self.typecast: Optional[str] = None
24
+ return
25
+
26
+ def cast(self, type) -> 'Column':
27
+ self.typecast = typename(type)
28
+ return self
29
+
30
+ def compile(self, compiler: Compiler) -> str:
31
+ return '{}{}'.format(
32
+ identifier(self.name),
33
+ '::{}'.format(self.typecast) if self.typecast else '',
34
+ )
@@ -0,0 +1,161 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABC,
5
+ abstractmethod,
6
+ )
7
+ import re
8
+ from typing import (
9
+ Any,
10
+ Dict,
11
+ Optional,
12
+ )
13
+
14
+ from psycopg import (
15
+ adapters as default_adapters,
16
+ sql,
17
+ )
18
+ from psycopg.adapt import AdaptersMap
19
+
20
+ from .Adapters import register
21
+
22
+ __all__ = (
23
+ 'Compilable',
24
+ 'Compiler',
25
+ 'identifier',
26
+ 'qualified',
27
+ 'quote',
28
+ 'typename',
29
+ 'valuetype',
30
+ )
31
+
32
+
33
+ def quote(name: str) -> str:
34
+ """Quote an identifier for use inside a bound text value such as a regclass name."""
35
+ if not isinstance(name, str) or not name or '\x00' in name:
36
+ raise ValueError('SQL identifiers must be nonempty strings without null characters')
37
+ return '"{}"'.format(name.replace('"', '""'))
38
+
39
+
40
+ def identifier(name: str) -> str:
41
+ """Quote an identifier and escape percent signs for parameterized execution."""
42
+ return quote(name).replace('%', '%%')
43
+
44
+
45
+ def qualified(
46
+ name: str,
47
+ schema: Optional[str] = None,
48
+ ) -> str:
49
+ if schema is None:
50
+ return identifier(name)
51
+ return '{}.{}'.format(identifier(schema), identifier(name))
52
+
53
+
54
+ def typename(
55
+ value,
56
+ escape: bool = True,
57
+ ) -> str:
58
+ """Resolve a SQL type declaration from a string, Type class, or Type instance."""
59
+ from .Type import Type
60
+ if isinstance(value, str):
61
+ if not value.strip() or '\x00' in value or ';' in value or '--' in value or '/*' in value:
62
+ raise ValueError('SQL type names must be nonempty strings without null characters, semicolons, or comments')
63
+ text = value
64
+ elif isinstance(value, Type) or (isinstance(value, type) and issubclass(value, Type)):
65
+ text = value.typestr
66
+ if not text:
67
+ raise ValueError('{} has no SQL type'.format(value.__name__ if isinstance(value, type) else type(value).__name__))
68
+ else:
69
+ raise TypeError('A SQL type must be a string or a PostgreSQL Type')
70
+ return text.replace('%', '%%') if escape else text
71
+
72
+
73
+ UNBOUNDED = {
74
+ 'CHAR': 'BPCHAR',
75
+ 'CHARACTER': 'BPCHAR',
76
+ 'BIT': 'VARBIT',
77
+ }
78
+
79
+
80
+ def valuetype(value) -> str:
81
+ """Resolve a cast for bound column values without length or precision modifiers.
82
+
83
+ An explicit cast such as ::VARCHAR(10) silently truncates, so values are cast to the
84
+ unbounded type and PostgreSQL applies its assignment and comparison rules instead.
85
+ """
86
+ text = re.sub(r'\s*\(\s*\d+\s*(?:,\s*-?\d+\s*)?\)', '', typename(value, escape=False))
87
+ text = re.sub(
88
+ r'^(CHARACTER|CHAR|BIT)(?=$|\[)',
89
+ lambda match: UNBOUNDED[match.group(1).upper()],
90
+ text,
91
+ flags=re.IGNORECASE,
92
+ )
93
+ return text
94
+
95
+
96
+ class LiteralContext:
97
+ """Adaptation context used to render DDL literals with DefPixel dumpers."""
98
+
99
+ def __init__(self):
100
+ self.adapters = AdaptersMap(default_adapters)
101
+ self.connection = None
102
+ register(self.adapters)
103
+ return
104
+
105
+
106
+ class Compilable(ABC):
107
+ """Object that renders itself into SQL through a Compiler."""
108
+
109
+ @abstractmethod
110
+ def compile(self, compiler: 'Compiler') -> str:
111
+ raise NotImplementedError('{} must be implemented compile'.format(self.__class__.__name__))
112
+
113
+ def __str__(self) -> str:
114
+ return self.compile(Compiler())
115
+
116
+
117
+ class Compiler:
118
+ """Collect bound parameters, or render literals for DDL that cannot take parameters.
119
+
120
+ qualify=False renders mapped columns without their table name, as required inside CREATE TABLE and CREATE INDEX.
121
+ """
122
+
123
+ context = None
124
+
125
+ def __init__(
126
+ self,
127
+ literal: bool = False,
128
+ qualify: bool = True,
129
+ ):
130
+ self.literal = literal
131
+ self.qualify = qualify
132
+ self.args: Dict[str, Any] = {}
133
+ return
134
+
135
+ def bind(
136
+ self,
137
+ value: Any,
138
+ cast=None,
139
+ ) -> str:
140
+ if isinstance(value, Compilable):
141
+ raise TypeError('Compilable objects must be compiled, not bound')
142
+ if self.literal:
143
+ if Compiler.context is None:
144
+ Compiler.context = LiteralContext()
145
+ text = sql.Literal(value).as_string(Compiler.context).replace('%', '%%')
146
+ else:
147
+ name = 'p{}'.format(len(self.args))
148
+ self.args[name] = value
149
+ text = '%({})s'.format(name)
150
+ if cast is not None:
151
+ text = '{}::{}'.format(text, typename(cast))
152
+ return text
153
+
154
+ def compile(
155
+ self,
156
+ value: Any,
157
+ cast=None,
158
+ ) -> str:
159
+ if isinstance(value, Compilable):
160
+ return value.compile(self)
161
+ return self.bind(value, cast)
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections.abc import Mapping
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Mapping as TypingMapping,
8
+ Optional,
9
+ )
10
+
11
+ from DefPixel.DataAccessObject import Configuration as BaseConfiguration
12
+ from psycopg.adapt import (
13
+ Dumper,
14
+ Loader,
15
+ )
16
+
17
+ __all__ = (
18
+ 'Configuration',
19
+ )
20
+
21
+
22
+ def text(
23
+ name: str,
24
+ value: Any,
25
+ required: bool,
26
+ ) -> Optional[str]:
27
+ if value is None and not required:
28
+ return None
29
+ if not isinstance(value, str) or not value or '\x00' in value:
30
+ raise ValueError('{} must be a nonempty string without null characters'.format(name))
31
+ return value
32
+
33
+
34
+ def integer(name: str, value: Any) -> int:
35
+ if type(value) is not int:
36
+ raise TypeError('{} must be an integer'.format(name))
37
+ return value
38
+
39
+
40
+ def flag(name: str, value: Any) -> bool:
41
+ if type(value) is not bool:
42
+ raise TypeError('{} must be a bool'.format(name))
43
+ return value
44
+
45
+
46
+ class Configuration(BaseConfiguration):
47
+ """Configuration for PostgreSQL connections and optional connection pools."""
48
+
49
+ def __init__(
50
+ self,
51
+ host: str,
52
+ port: int = 5432,
53
+ database: Optional[str] = None,
54
+ username: Optional[str] = None,
55
+ password: Optional[str] = None,
56
+ autocommit: bool = False,
57
+ pool: bool = False,
58
+ min: int = 0,
59
+ max: int = 0,
60
+ dumpers: Optional[TypingMapping] = None,
61
+ loaders: Optional[TypingMapping] = None,
62
+ timezone: Optional[str] = None,
63
+ ):
64
+ self.host = text('host', host, True)
65
+ self.database = text('database', database, True)
66
+ self.port = integer('port', port)
67
+ if not 1 <= self.port <= 65535:
68
+ raise ValueError('port must be between 1 and 65535')
69
+ self.username = text('username', username, False)
70
+ self.password = text('password', password, False)
71
+ self.autocommit = flag('autocommit', autocommit)
72
+ self.pool = flag('pool', pool)
73
+ self.min = integer('min', min)
74
+ self.max = integer('max', max)
75
+ if self.pool and (self.max < 1 or not 0 <= self.min <= self.max):
76
+ raise ValueError('A connection pool requires 0 <= min <= max and max >= 1')
77
+ for name, value, base in (('dumpers', dumpers, Dumper), ('loaders', loaders, Loader)):
78
+ if value is None:
79
+ continue
80
+ if not isinstance(value, Mapping):
81
+ raise TypeError('{} must be a mapping'.format(name))
82
+ for adapter in value.values():
83
+ if not isinstance(adapter, type) or not issubclass(adapter, base):
84
+ raise TypeError('{} must map to psycopg {} classes'.format(name, base.__name__))
85
+ self.dumpers = dict(dumpers or {})
86
+ self.loaders = dict(loaders or {})
87
+ self.timezone = text('timezone', timezone, False)
88
+ return
89
+
90
+ def __repr__(self) -> str:
91
+ return '{}(host={!r}, port={!r}, database={!r}, username={!r}, autocommit={!r}, pool={!r})'.format(
92
+ self.__class__.__name__,
93
+ self.host,
94
+ self.port,
95
+ self.database,
96
+ self.username,
97
+ self.autocommit,
98
+ self.pool,
99
+ )
100
+
101
+ def parameters(self) -> Dict[str, Any]:
102
+ """Return driver keyword arguments; secrets never pass through a connection string."""
103
+ kwargs = {
104
+ 'host': self.host,
105
+ 'port': self.port,
106
+ 'dbname': self.database,
107
+ }
108
+ if self.username is not None:
109
+ kwargs['user'] = self.username
110
+ if self.password is not None:
111
+ kwargs['password'] = self.password
112
+ if self.timezone is not None:
113
+ kwargs['options'] = '-c TimeZone={}'.format(
114
+ self.timezone.replace('\\', '\\\\').replace(' ', '\\ '),
115
+ )
116
+ return kwargs
@@ -0,0 +1,138 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from itertools import count
4
+
5
+ from psycopg import connect
6
+ from psycopg.pq import TransactionStatus
7
+ from psycopg.rows import dict_row
8
+
9
+ from DefPixel.DataAccessObject import Connection as BaseConnection
10
+ from DefPixel.DataAccessObject.Properties.Database import (
11
+ Database,
12
+ Run,
13
+ )
14
+ from .Adapters import register
15
+ from .Configuration import Configuration
16
+ from .Context import Context
17
+ from .Cursor import Cursor
18
+ from .Pool import Pool
19
+ from .Session import Session
20
+
21
+ __all__ = (
22
+ 'Connection',
23
+ )
24
+
25
+
26
+ class Connection(BaseConnection, Database, Run):
27
+ """PostgreSQL connection with explicit lifecycle and transaction management."""
28
+
29
+ def __init__(self, conf: Configuration):
30
+ if not isinstance(conf, Configuration):
31
+ raise TypeError('Connection requires a PostgreSQL Configuration')
32
+ self.conf = conf
33
+ self.connection = None
34
+ self.savepoints = count()
35
+ return
36
+
37
+ def connect(self):
38
+ if self.connection is not None:
39
+ raise RuntimeError('The PostgreSQL connection is already open')
40
+ if self.conf.pool:
41
+ self.connection = Pool.get(self.conf)
42
+ return
43
+ connection = connect(
44
+ autocommit=self.conf.autocommit,
45
+ row_factory=dict_row,
46
+ **self.conf.parameters(),
47
+ )
48
+ try:
49
+ register(connection.adapters, self.conf.dumpers, self.conf.loaders)
50
+ except BaseException:
51
+ connection.close()
52
+ raise
53
+ self.connection = connection
54
+ return
55
+
56
+ def active(self):
57
+ if self.connection is None or self.connection.closed:
58
+ raise RuntimeError('The PostgreSQL connection is not open')
59
+ return self.connection
60
+
61
+ def close(self):
62
+ """Close or release the connection; an open transaction is rolled back, never committed."""
63
+ if self.connection is None:
64
+ return
65
+ connection = self.connection
66
+ self.connection = None
67
+ try:
68
+ if not connection.closed and connection.info.transaction_status != TransactionStatus.IDLE:
69
+ connection.rollback()
70
+ if not connection.closed and connection.autocommit != self.conf.autocommit:
71
+ connection.autocommit = self.conf.autocommit
72
+ finally:
73
+ if self.conf.pool:
74
+ Pool.release(self.conf, connection)
75
+ else:
76
+ connection.close()
77
+ return
78
+
79
+ def autocommit(self, operation: str):
80
+ if self.conf.autocommit:
81
+ raise RuntimeError('{} is not available in autocommit mode; use session() for a transaction'.format(operation))
82
+ return self.active()
83
+
84
+ def begin(self):
85
+ connection = self.autocommit('begin')
86
+ if connection.info.transaction_status != TransactionStatus.IDLE:
87
+ raise RuntimeError('A transaction is already in progress')
88
+ # An empty statement makes psycopg open the transaction without a server warning.
89
+ connection.execute('')
90
+ return
91
+
92
+ def execute(
93
+ self,
94
+ sql: str,
95
+ *args,
96
+ **kwargs,
97
+ ) -> Context:
98
+ return self.cursor().execute(sql, *args, **kwargs)
99
+
100
+ def executes(
101
+ self,
102
+ sql: str,
103
+ *args,
104
+ ) -> Context:
105
+ return self.cursor().executes(sql, *args)
106
+
107
+ def run(
108
+ self,
109
+ executor,
110
+ filter=None,
111
+ fetch=None,
112
+ ):
113
+ cursor = self.cursor()
114
+ try:
115
+ return cursor.run(
116
+ executor,
117
+ filter=filter,
118
+ fetch=fetch,
119
+ )
120
+ finally:
121
+ cursor.close()
122
+
123
+ def cursor(self) -> Cursor:
124
+ return Cursor(self.active().cursor())
125
+
126
+ def session(self) -> Session:
127
+ return Session(
128
+ self.active(),
129
+ lambda: 'defpixel_savepoint_{}'.format(next(self.savepoints)),
130
+ )
131
+
132
+ def commit(self):
133
+ self.autocommit('commit').commit()
134
+ return
135
+
136
+ def rollback(self):
137
+ self.autocommit('rollback').rollback()
138
+ return
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import ABC
4
+
5
+ from .Compiler import identifier
6
+
7
+ __all__ = (
8
+ 'Constraint',
9
+ )
10
+
11
+
12
+ class Constraint(ABC):
13
+ """Named table constraint."""
14
+
15
+ def __init__(self, name: str):
16
+ identifier(name)
17
+ self.name = name
18
+ return
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ..Compiler import Compilable
4
+ from ..Constraint import Constraint
5
+
6
+ __all__ = (
7
+ 'Check',
8
+ )
9
+
10
+
11
+ class Check(Constraint):
12
+ """Named check constraint; values in the expression are rendered as DDL literals."""
13
+
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ expr,
18
+ ):
19
+ super().__init__(name)
20
+ if not isinstance(expr, Compilable):
21
+ raise TypeError('Check requires a SQL expression')
22
+ self.expr = expr
23
+ return
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from ..Column import Column
6
+ from ..Type import Type
7
+
8
+ __all__ = (
9
+ 'columns',
10
+ )
11
+
12
+
13
+ def columns(values, name: str = 'cols') -> list:
14
+ """Normalize constraint columns given as names, Column objects, or column specifications."""
15
+ if isinstance(values, Sequence) and not isinstance(values, str):
16
+ values = list(values)
17
+ else:
18
+ values = [values]
19
+ if not values:
20
+ raise ValueError('{} requires at least one column'.format(name))
21
+ for value in values:
22
+ if not isinstance(value, (str, Column, Type)):
23
+ raise TypeError('{} must contain column names, Column objects, or column specifications'.format(name))
24
+ return values