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.
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/Array.py +16 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/JavaScriptObjectNotation.py +74 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/__init__.py +51 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Column.py +34 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Compiler.py +161 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Configuration.py +116 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Connection.py +138 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraint.py +18 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Check.py +23 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Columns.py +24 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/ForeignKey.py +56 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/PrimaryKey.py +21 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Unique.py +25 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/__init__.py +13 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Context.py +48 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Cursor.py +123 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Create.py +181 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Delete.py +31 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Drop.py +53 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Get.py +24 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Insert.py +98 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Query.py +37 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Select.py +122 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Update.py +57 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/__init__.py +19 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Expr.py +11 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Common.py +180 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Compare.py +87 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Condition.py +155 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/JavaScriptObjectNotation.py +53 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Operator.py +42 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/String.py +48 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/__init__.py +75 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Function.py +11 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Common.py +142 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/DateTime.py +219 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/JavaScriptObjectNotation.py +100 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Sequence.py +55 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/__init__.py +91 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Index.py +135 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/FullOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Inner.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Join.py +41 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/LeftOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/RightOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/__init__.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Mapping.py +359 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Orders.py +59 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Pool.py +84 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Schema.py +28 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Sequence.py +55 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Session.py +84 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Type.py +113 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Array.py +61 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Binary.py +34 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Bool.py +29 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/DateTime.py +120 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/JavaScriptObjectNotation.py +52 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Numeric.py +151 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Size.py +13 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/String.py +80 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/__init__.py +76 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/__init__.py +85 -0
- defpixel_postgresql-0.1.6.dist-info/METADATA +184 -0
- defpixel_postgresql-0.1.6.dist-info/RECORD +68 -0
- defpixel_postgresql-0.1.6.dist-info/WHEEL +6 -0
- defpixel_postgresql-0.1.6.dist-info/licenses/LICENSE +674 -0
- 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
|