DefPixel-PostgreSQL 0.1.6__91-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.6.dist-info/METADATA +184 -0
  65. defpixel_postgresql-0.1.6.dist-info/RECORD +68 -0
  66. defpixel_postgresql-0.1.6.dist-info/WHEEL +6 -0
  67. defpixel_postgresql-0.1.6.dist-info/licenses/LICENSE +674 -0
  68. defpixel_postgresql-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,359 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from copy import (
4
+ copy,
5
+ deepcopy,
6
+ )
7
+ from dataclasses import (
8
+ MISSING,
9
+ field,
10
+ fields,
11
+ is_dataclass,
12
+ )
13
+ from types import MemberDescriptorType
14
+ from typing import (
15
+ Any,
16
+ Callable,
17
+ Dict,
18
+ NamedTuple,
19
+ Optional,
20
+ Sequence as TypingSequence,
21
+ Type as PythonType,
22
+ TypeVar,
23
+ )
24
+
25
+ from .Compiler import (
26
+ Compilable,
27
+ identifier,
28
+ qualified,
29
+ )
30
+ from .Constraint import Constraint
31
+ from .Index import Index
32
+ from .Schema import Schema
33
+ from .Sequence import Sequence
34
+ from .Type import Type
35
+
36
+ __all__ = (
37
+ 'column',
38
+ 'table',
39
+ 'view',
40
+ )
41
+
42
+ METADATA = 'postgresql'
43
+ MAPPING = '__postgresql__'
44
+ T = TypeVar('T')
45
+
46
+
47
+ class Statement(NamedTuple):
48
+ """Satisfy the single-execution contract while preserving query and argument unpacking."""
49
+
50
+ query: str
51
+ args: Any = ()
52
+
53
+
54
+ class ColumnAccess:
55
+ """Expose a column on class access and a standard field value on instance access."""
56
+
57
+ def __init__(
58
+ self,
59
+ name: str,
60
+ spec: Type,
61
+ original: Any,
62
+ default: Any,
63
+ ):
64
+ self.name = name
65
+ self.spec = spec
66
+ self.original = original
67
+ self.default = default
68
+ return
69
+
70
+ def __get__(
71
+ self,
72
+ instance,
73
+ owner=None,
74
+ ):
75
+ if instance is None:
76
+ return self.spec
77
+ if self.original is not None:
78
+ return self.original.__get__(instance, owner)
79
+ if self.name in instance.__dict__:
80
+ return instance.__dict__[self.name]
81
+ if self.default is not MISSING:
82
+ return self.default
83
+ raise AttributeError(self.name)
84
+
85
+ def __set__(
86
+ self,
87
+ instance,
88
+ value,
89
+ ):
90
+ if self.original is not None:
91
+ self.original.__set__(instance, value)
92
+ else:
93
+ instance.__dict__[self.name] = value
94
+ return
95
+
96
+ def __delete__(self, instance):
97
+ if self.original is not None:
98
+ self.original.__delete__(instance)
99
+ else:
100
+ try:
101
+ del instance.__dict__[self.name]
102
+ except KeyError:
103
+ raise AttributeError(self.name) from None
104
+ return
105
+
106
+
107
+ class Mapping:
108
+ """Explicit mapping between a dataclass and PostgreSQL storage."""
109
+
110
+ def __init__(
111
+ self,
112
+ model: PythonType,
113
+ name: str,
114
+ kind: str,
115
+ schema: Optional[str],
116
+ columns: Dict[str, Type],
117
+ sequences: TypingSequence[Sequence],
118
+ constraints: TypingSequence[Constraint],
119
+ indexes: TypingSequence[Index],
120
+ executor=None,
121
+ description=None,
122
+ ):
123
+ self.model = model
124
+ self.name = name
125
+ self.kind = kind
126
+ self.schema = schema
127
+ self.columns = columns
128
+ self.sequences = sequences
129
+ self.constraints = constraints
130
+ self.indexes = indexes
131
+ self.executor = executor
132
+ self.description = description
133
+ return
134
+
135
+ @property
136
+ def target(self) -> str:
137
+ return qualified(self.name, self.schema)
138
+
139
+ def key(self, name: str) -> str:
140
+ """Resolve a field name or column key to the column key."""
141
+ if name in self.columns:
142
+ return self.columns[name].key
143
+ if name in {spec.key for spec in self.columns.values()}:
144
+ return name
145
+ raise ValueError('Column is not mapped: {}'.format(name))
146
+
147
+ def values(self, values):
148
+ unknown = set(values) - self.columns.keys()
149
+ if unknown:
150
+ raise ValueError('Unmapped fields: {}'.format(', '.join(sorted(unknown))))
151
+ return {
152
+ self.columns[name].key: (self.columns[name], self.columns[name].encode(value))
153
+ for name, value in values.items()
154
+ }
155
+
156
+ def restore(self, row):
157
+ members = {member.name: member for member in fields(self.model)}
158
+ values = {}
159
+ deferred = {}
160
+ by_column = {spec.key: name for name, spec in self.columns.items()}
161
+ for key, value in row.items():
162
+ if key not in by_column:
163
+ raise ValueError('Result column is not mapped to the model: {}'.format(key))
164
+ name = by_column[key]
165
+ value = self.columns[name].validate(value)
166
+ if members[name].init:
167
+ values[name] = value
168
+ else:
169
+ deferred[name] = value
170
+ for name, member in members.items():
171
+ if (
172
+ member.init and name not in values
173
+ and member.default is MISSING and member.default_factory is MISSING
174
+ ):
175
+ raise ValueError('Missing field required for object restoration: {}'.format(name))
176
+ instance = self.model(**values)
177
+ for name, value in deferred.items():
178
+ object.__setattr__(instance, name, value)
179
+ return instance
180
+
181
+
182
+ def column(
183
+ spec: Type,
184
+ *,
185
+ validator=None,
186
+ description: Optional[str] = None,
187
+ ) -> Any:
188
+ """Attach a PostgreSQL specification and its default to a standard Field."""
189
+ if not isinstance(spec, Type):
190
+ raise TypeError('column requires a PostgreSQL Type specification')
191
+ spec = copy(spec)
192
+ if validator is not None:
193
+ spec.validator = validator
194
+ metadata = {
195
+ METADATA: spec,
196
+ 'description': description,
197
+ }
198
+ default = spec.coldef
199
+ if isinstance(default, Compilable):
200
+ # Server-side defaults are computed by PostgreSQL; the dataclass value stays empty.
201
+ return field(default=None, metadata=metadata)
202
+ if isinstance(default, (list, dict, set)):
203
+ return field(default_factory=lambda: deepcopy(default), metadata=metadata)
204
+ return field(default=default, metadata=metadata)
205
+
206
+
207
+ def inspect_model(
208
+ model: PythonType,
209
+ kind: Optional[str] = None,
210
+ ) -> Mapping:
211
+ if not isinstance(model, type) or not is_dataclass(model):
212
+ raise TypeError('ORM models must be dataclass classes')
213
+ mapping = vars(model).get(MAPPING)
214
+ if not isinstance(mapping, Mapping) or mapping.model is not model:
215
+ raise ValueError('{} has no PostgreSQL @table or @view mapping'.format(model.__name__))
216
+ if kind is not None and mapping.kind != kind:
217
+ raise ValueError('A {} operation cannot use a {} mapping'.format(kind, mapping.kind))
218
+ return mapping
219
+
220
+
221
+ def items(value, kind, name: str) -> tuple:
222
+ if value is None:
223
+ return ()
224
+ if isinstance(value, kind):
225
+ return (value,)
226
+ value = tuple(value)
227
+ for item in value:
228
+ if not isinstance(item, kind):
229
+ raise TypeError('{} must contain {} objects'.format(name, kind.__name__))
230
+ return value
231
+
232
+
233
+ def mapped(
234
+ model: PythonType[T],
235
+ name,
236
+ kind,
237
+ schema,
238
+ sequences,
239
+ constraints,
240
+ indexes,
241
+ executor,
242
+ description,
243
+ ) -> PythonType[T]:
244
+ if not isinstance(model, type) or not is_dataclass(model):
245
+ raise TypeError('@{} must be applied after @dataclass'.format(kind))
246
+ if MAPPING in vars(model):
247
+ raise ValueError('The model already has a PostgreSQL mapping')
248
+ name = model.__name__ if name is None else name
249
+ identifier(name)
250
+ if isinstance(schema, Schema):
251
+ schema = schema.name
252
+ if schema is not None:
253
+ identifier(schema)
254
+ sequences = items(sequences, Sequence, 'sequences')
255
+ for sequence in sequences:
256
+ if sequence.schema is not None and sequence.schema != schema:
257
+ raise ValueError('Sequence {} already belongs to schema {}'.format(sequence.name, sequence.schema))
258
+ sequence.schema = schema
259
+ columns = {}
260
+ accessors = {}
261
+ keys = set()
262
+ for member in fields(model):
263
+ if member.name == MAPPING:
264
+ raise ValueError('The reserved mapping attribute cannot be used as a field name')
265
+ if METADATA not in member.metadata:
266
+ continue
267
+ spec = copy(member.metadata[METADATA])
268
+ spec.key = member.name if spec.key is None else spec.key
269
+ identifier(spec.key)
270
+ if spec.key in keys:
271
+ raise ValueError('Duplicate column: {}'.format(spec.key))
272
+ keys.add(spec.key)
273
+ spec.model = name
274
+ spec.schema = schema
275
+ columns[member.name] = spec
276
+ original = None
277
+ for base in model.__mro__:
278
+ attribute = vars(base).get(member.name)
279
+ if isinstance(attribute, ColumnAccess):
280
+ original = attribute.original
281
+ break
282
+ if isinstance(attribute, MemberDescriptorType):
283
+ original = attribute
284
+ break
285
+ accessors[member.name] = ColumnAccess(
286
+ member.name,
287
+ spec,
288
+ original,
289
+ member.default,
290
+ )
291
+ if not columns:
292
+ raise ValueError('No fields are declared with column()')
293
+ mapping = Mapping(
294
+ model,
295
+ name,
296
+ kind,
297
+ schema,
298
+ columns,
299
+ sequences,
300
+ items(constraints, Constraint, 'constraints'),
301
+ items(indexes, Index, 'indexes'),
302
+ executor,
303
+ description,
304
+ )
305
+ setattr(model, MAPPING, mapping)
306
+ for key, accessor in accessors.items():
307
+ setattr(model, key, accessor)
308
+ return model
309
+
310
+
311
+ def table(
312
+ name=None,
313
+ *,
314
+ schema=None,
315
+ sequences=None,
316
+ constraints=None,
317
+ indexes=None,
318
+ description=None,
319
+ ) -> Callable[[PythonType[T]], PythonType[T]]:
320
+ """Map a processed dataclass to a table and return the same class."""
321
+ def decorate(model: PythonType[T]) -> PythonType[T]:
322
+ return mapped(
323
+ model,
324
+ name,
325
+ 'table',
326
+ schema,
327
+ sequences,
328
+ constraints,
329
+ indexes,
330
+ None,
331
+ description,
332
+ )
333
+ return decorate
334
+
335
+
336
+ def view(
337
+ *,
338
+ executor,
339
+ name=None,
340
+ schema=None,
341
+ description=None,
342
+ ) -> Callable[[PythonType[T]], PythonType[T]]:
343
+ """Map a processed dataclass to a view defined by a query Executor."""
344
+ def decorate(model: PythonType[T]) -> PythonType[T]:
345
+ from DefPixel.DataAccessObject.Properties.Database import Executor
346
+ if not isinstance(executor, Executor) or not isinstance(executor, Compilable):
347
+ raise TypeError('View definitions require a PostgreSQL query Executor')
348
+ return mapped(
349
+ model,
350
+ name,
351
+ 'view',
352
+ schema,
353
+ None,
354
+ None,
355
+ None,
356
+ executor,
357
+ description,
358
+ )
359
+ return decorate
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from .Column import Column
6
+ from .Compiler import (
7
+ Compilable,
8
+ Compiler,
9
+ )
10
+ from .Expr import Expr
11
+
12
+ __all__ = (
13
+ 'Ascend',
14
+ 'Descend',
15
+ 'Random',
16
+ )
17
+
18
+ NULLS = ('FIRST', 'LAST')
19
+
20
+
21
+ class Order(Expr):
22
+ direction = ''
23
+
24
+ def __init__(
25
+ self,
26
+ col,
27
+ null: Optional[str] = 'LAST',
28
+ ):
29
+ if isinstance(col, str):
30
+ col = Column(col)
31
+ if not isinstance(col, Compilable):
32
+ raise TypeError('Orders require a column name or SQL expression')
33
+ if null is not None:
34
+ if not isinstance(null, str) or null.upper() not in NULLS:
35
+ raise ValueError('null must be FIRST, LAST, or None')
36
+ null = null.upper()
37
+ self.col = col
38
+ self.null = null
39
+ return
40
+
41
+ def compile(self, compiler: Compiler) -> str:
42
+ return '{} {}{}'.format(
43
+ compiler.compile(self.col),
44
+ self.direction,
45
+ ' NULLS {}'.format(self.null) if self.null else '',
46
+ )
47
+
48
+
49
+ class Ascend(Order):
50
+ direction = 'ASC'
51
+
52
+
53
+ class Descend(Order):
54
+ direction = 'DESC'
55
+
56
+
57
+ class Random(Expr):
58
+ def compile(self, compiler: Compiler) -> str:
59
+ return 'RANDOM()'
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from threading import Lock
4
+ from typing import (
5
+ Dict,
6
+ Optional,
7
+ Tuple,
8
+ )
9
+
10
+ from psycopg.rows import dict_row
11
+ from psycopg import Connection
12
+ from psycopg_pool import ConnectionPool
13
+
14
+ from .Adapters import register
15
+ from .Configuration import Configuration
16
+
17
+ __all__ = (
18
+ 'Pool',
19
+ )
20
+
21
+
22
+ class Pool:
23
+ """Process-wide registry of connection pools keyed by Configuration object."""
24
+
25
+ # Entries keep their Configuration alive so its id cannot be reused by another object.
26
+ pools: Dict[int, Tuple[Configuration, ConnectionPool]] = {}
27
+ borrowed: Dict[Connection, Tuple[Configuration, ConnectionPool]] = {}
28
+ lock = Lock()
29
+
30
+ @classmethod
31
+ def get(cls, conf: Configuration):
32
+ """Acquire a connection from the pool for the configuration, creating the pool on first use."""
33
+ with cls.lock:
34
+ entry = cls.pools.get(id(conf))
35
+ pool = entry[1] if entry is not None else None
36
+ if pool is None:
37
+ def configure(connection):
38
+ register(connection.adapters, conf.dumpers, conf.loaders)
39
+ return
40
+ pool = ConnectionPool(
41
+ kwargs=dict(
42
+ conf.parameters(),
43
+ autocommit=conf.autocommit,
44
+ row_factory=dict_row,
45
+ ),
46
+ min_size=conf.min,
47
+ max_size=conf.max,
48
+ configure=configure,
49
+ open=True,
50
+ )
51
+ cls.pools[id(conf)] = (conf, pool)
52
+ connection = pool.getconn()
53
+ with cls.lock:
54
+ cls.borrowed[connection] = (conf, pool)
55
+ return connection
56
+
57
+ @classmethod
58
+ def release(
59
+ cls,
60
+ conf: Configuration,
61
+ connection,
62
+ ):
63
+ """Return to the originating pool, including after shutdown or recreation."""
64
+ with cls.lock:
65
+ entry = cls.borrowed.get(connection)
66
+ if entry is None or entry[0] is not conf:
67
+ raise RuntimeError('The connection was not borrowed for the configuration')
68
+ entry[1].putconn(connection)
69
+ del cls.borrowed[connection]
70
+ return
71
+
72
+ @classmethod
73
+ def close(cls, conf: Optional[Configuration] = None):
74
+ """Close the pool for one configuration, or every pool when no configuration is given."""
75
+ with cls.lock:
76
+ if conf is None:
77
+ pools = [pool for _, pool in cls.pools.values()]
78
+ cls.pools.clear()
79
+ else:
80
+ entry = cls.pools.pop(id(conf), None)
81
+ pools = [entry[1]] if entry is not None else []
82
+ for pool in pools:
83
+ pool.close()
84
+ return
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from .Compiler import identifier
6
+
7
+ __all__ = (
8
+ 'Schema',
9
+ )
10
+
11
+
12
+ class Schema:
13
+ """PostgreSQL schema declaration usable with Create and Drop."""
14
+
15
+ def __init__(
16
+ self,
17
+ name: str,
18
+ username: Optional[str] = None,
19
+ ):
20
+ identifier(name)
21
+ if username is not None:
22
+ identifier(username)
23
+ self.name = name
24
+ self.username = username
25
+ return
26
+
27
+ def __str__(self) -> str:
28
+ return self.name
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Optional
4
+
5
+ from .Compiler import (
6
+ identifier,
7
+ typename,
8
+ )
9
+
10
+ __all__ = (
11
+ 'Sequence',
12
+ )
13
+
14
+
15
+ def option(name: str, value, null: bool = False):
16
+ if value is None and null:
17
+ return None
18
+ if type(value) is not int:
19
+ raise TypeError('Sequence {} must be an int'.format(name))
20
+ return value
21
+
22
+
23
+ class Sequence:
24
+ """PostgreSQL sequence declaration; table mappings assign the table schema."""
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ type=None,
30
+ increment: int = 1,
31
+ min: Optional[int] = 1,
32
+ max: Optional[int] = None,
33
+ start: Optional[int] = 1,
34
+ notexists: bool = True,
35
+ ):
36
+ identifier(name)
37
+ if type is None:
38
+ from .Types import BIGINT
39
+ type = BIGINT
40
+ self.name = name
41
+ self.schema: Optional[str] = None
42
+ self.type = typename(type, escape=False)
43
+ self.increment = option('increment', increment)
44
+ if self.increment == 0:
45
+ raise ValueError('Sequence increment cannot be zero')
46
+ self.min = option('min', min, True)
47
+ self.max = option('max', max, True)
48
+ self.start = option('start', start, True)
49
+ if not isinstance(notexists, bool):
50
+ raise TypeError('notexists must be a bool')
51
+ self.notexists = notexists
52
+ return
53
+
54
+ def __str__(self) -> str:
55
+ return self.name
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import Callable
4
+
5
+ from psycopg.pq import TransactionStatus
6
+
7
+ from DefPixel.DataAccessObject.Properties.Database import Session as BaseSession
8
+ from .Compiler import identifier
9
+ from .Cursor import Cursor
10
+
11
+ __all__ = (
12
+ 'Session',
13
+ )
14
+
15
+
16
+ class Session(Cursor, BaseSession):
17
+ """Transaction scope: owns a transaction when none is open, otherwise uses a SAVEPOINT."""
18
+
19
+ def __init__(
20
+ self,
21
+ connection,
22
+ savepoint: Callable[[], str],
23
+ ):
24
+ self.connection = connection
25
+ self.name = savepoint
26
+ self.savepoint = None
27
+ self.autocommit = None
28
+ self.entered = False
29
+ super().__init__(connection.cursor())
30
+ return
31
+
32
+ def __enter__(self):
33
+ if self.entered:
34
+ raise RuntimeError('The session has already been entered')
35
+ status = self.connection.info.transaction_status
36
+ if status == TransactionStatus.INERROR:
37
+ raise RuntimeError('The current transaction is aborted; roll it back before starting a session')
38
+ if status == TransactionStatus.INTRANS:
39
+ self.savepoint = identifier(self.name())
40
+ self.connection.execute('SAVEPOINT {}'.format(self.savepoint))
41
+ elif status == TransactionStatus.IDLE:
42
+ if self.connection.autocommit:
43
+ self.autocommit = True
44
+ self.connection.autocommit = False
45
+ try:
46
+ # An empty statement makes psycopg open the transaction without a server warning.
47
+ self.connection.execute('')
48
+ except BaseException:
49
+ self.restore()
50
+ raise
51
+ else:
52
+ raise RuntimeError('A session cannot start while the connection is {}'.format(status.name))
53
+ self.entered = True
54
+ return self
55
+
56
+ def __exit__(
57
+ self,
58
+ exception_type,
59
+ exception,
60
+ traceback,
61
+ ):
62
+ try:
63
+ if self.savepoint is not None:
64
+ if exception_type is not None:
65
+ self.connection.execute('ROLLBACK TO SAVEPOINT {}'.format(self.savepoint))
66
+ self.connection.execute('RELEASE SAVEPOINT {}'.format(self.savepoint))
67
+ elif exception_type is None:
68
+ self.connection.commit()
69
+ else:
70
+ self.connection.rollback()
71
+ finally:
72
+ try:
73
+ self.close()
74
+ finally:
75
+ self.restore()
76
+ return False
77
+
78
+ def restore(self):
79
+ if self.autocommit is not None:
80
+ if self.connection.info.transaction_status != TransactionStatus.IDLE:
81
+ self.connection.rollback()
82
+ self.connection.autocommit = self.autocommit
83
+ self.autocommit = None
84
+ return