sqlobject 3.13.1__py2.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.
- sqlobject/__init__.py +15 -0
- sqlobject/__version__.py +8 -0
- sqlobject/boundattributes.py +124 -0
- sqlobject/cache.py +376 -0
- sqlobject/classregistry.py +138 -0
- sqlobject/col.py +2187 -0
- sqlobject/compat.py +59 -0
- sqlobject/conftest.py +64 -0
- sqlobject/constraints.py +77 -0
- sqlobject/converters.py +278 -0
- sqlobject/dbconnection.py +1137 -0
- sqlobject/dberrors.py +54 -0
- sqlobject/declarative.py +211 -0
- sqlobject/events.py +365 -0
- sqlobject/firebird/__init__.py +8 -0
- sqlobject/firebird/firebirdconnection.py +463 -0
- sqlobject/include/__init__.py +1 -0
- sqlobject/include/hashcol.py +109 -0
- sqlobject/include/tests/__init__.py +1 -0
- sqlobject/include/tests/test_hashcol.py +84 -0
- sqlobject/index.py +189 -0
- sqlobject/inheritance/__init__.py +551 -0
- sqlobject/inheritance/iteration.py +106 -0
- sqlobject/inheritance/tests/__init__.py +1 -0
- sqlobject/inheritance/tests/test_aggregates.py +23 -0
- sqlobject/inheritance/tests/test_asdict.py +48 -0
- sqlobject/inheritance/tests/test_deep_inheritance.py +114 -0
- sqlobject/inheritance/tests/test_destroy_cascade.py +23 -0
- sqlobject/inheritance/tests/test_foreignKey.py +94 -0
- sqlobject/inheritance/tests/test_indexes.py +42 -0
- sqlobject/inheritance/tests/test_inheritance.py +164 -0
- sqlobject/inheritance/tests/test_inheritance_tree.py +50 -0
- sqlobject/joins.py +656 -0
- sqlobject/main.py +1880 -0
- sqlobject/manager/__init__.py +1 -0
- sqlobject/manager/command.py +1309 -0
- sqlobject/maxdb/__init__.py +8 -0
- sqlobject/maxdb/maxdbconnection.py +320 -0
- sqlobject/maxdb/readme.txt +27 -0
- sqlobject/mssql/__init__.py +8 -0
- sqlobject/mssql/mssqlconnection.py +417 -0
- sqlobject/mysql/__init__.py +8 -0
- sqlobject/mysql/mysqlconnection.py +562 -0
- sqlobject/postgres/__init__.py +8 -0
- sqlobject/postgres/pgconnection.py +619 -0
- sqlobject/sqlbuilder.py +1655 -0
- sqlobject/sqlite/__init__.py +8 -0
- sqlobject/sqlite/sqliteconnection.py +418 -0
- sqlobject/sresults.py +377 -0
- sqlobject/styles.py +174 -0
- sqlobject/sybase/__init__.py +8 -0
- sqlobject/sybase/sybaseconnection.py +183 -0
- sqlobject/tests/__init__.py +1 -0
- sqlobject/tests/dbtest.py +363 -0
- sqlobject/tests/test_ForeignKey.py +133 -0
- sqlobject/tests/test_ForeignKey_cascade.py +150 -0
- sqlobject/tests/test_NoneValuedResultItem.py +33 -0
- sqlobject/tests/test_SQLMultipleJoin.py +65 -0
- sqlobject/tests/test_SQLRelatedJoin.py +114 -0
- sqlobject/tests/test_SingleJoin.py +30 -0
- sqlobject/tests/test_aggregates.py +75 -0
- sqlobject/tests/test_aliases.py +49 -0
- sqlobject/tests/test_asdict.py +18 -0
- sqlobject/tests/test_auto.py +218 -0
- sqlobject/tests/test_basic.py +359 -0
- sqlobject/tests/test_blob.py +34 -0
- sqlobject/tests/test_boundattributes.py +67 -0
- sqlobject/tests/test_cache.py +57 -0
- sqlobject/tests/test_class_hash.py +22 -0
- sqlobject/tests/test_columns_order.py +18 -0
- sqlobject/tests/test_combining_joins.py +47 -0
- sqlobject/tests/test_comparison.py +24 -0
- sqlobject/tests/test_compat.py +10 -0
- sqlobject/tests/test_complex_sorting.py +77 -0
- sqlobject/tests/test_constraints.py +43 -0
- sqlobject/tests/test_converters.py +276 -0
- sqlobject/tests/test_create_drop.py +29 -0
- sqlobject/tests/test_csvexport.py +101 -0
- sqlobject/tests/test_csvimport.py +21 -0
- sqlobject/tests/test_cyclic_reference.py +55 -0
- sqlobject/tests/test_datetime.py +171 -0
- sqlobject/tests/test_dbconnection.py +17 -0
- sqlobject/tests/test_decimal.py +63 -0
- sqlobject/tests/test_declarative.py +83 -0
- sqlobject/tests/test_default_style.py +65 -0
- sqlobject/tests/test_delete.py +74 -0
- sqlobject/tests/test_distinct.py +36 -0
- sqlobject/tests/test_empty.py +22 -0
- sqlobject/tests/test_enum.py +75 -0
- sqlobject/tests/test_events.py +142 -0
- sqlobject/tests/test_exceptions.py +40 -0
- sqlobject/tests/test_expire.py +29 -0
- sqlobject/tests/test_groupBy.py +45 -0
- sqlobject/tests/test_identity.py +29 -0
- sqlobject/tests/test_indexes.py +100 -0
- sqlobject/tests/test_inheritance.py +35 -0
- sqlobject/tests/test_joins.py +160 -0
- sqlobject/tests/test_joins_conditional.py +143 -0
- sqlobject/tests/test_jsonbcol.py +53 -0
- sqlobject/tests/test_jsoncol.py +56 -0
- sqlobject/tests/test_lazy.py +165 -0
- sqlobject/tests/test_md5.py +13 -0
- sqlobject/tests/test_mysql.py +50 -0
- sqlobject/tests/test_new_joins.py +165 -0
- sqlobject/tests/test_parse_uri.py +147 -0
- sqlobject/tests/test_paste.py +92 -0
- sqlobject/tests/test_perConnection.py +20 -0
- sqlobject/tests/test_pickle.py +42 -0
- sqlobject/tests/test_picklecol.py +44 -0
- sqlobject/tests/test_postgres.py +82 -0
- sqlobject/tests/test_reparent_sqlmeta.py +33 -0
- sqlobject/tests/test_schema.py +30 -0
- sqlobject/tests/test_select.py +218 -0
- sqlobject/tests/test_select_through.py +63 -0
- sqlobject/tests/test_setters.py +23 -0
- sqlobject/tests/test_slice.py +59 -0
- sqlobject/tests/test_sorting.py +80 -0
- sqlobject/tests/test_sqlbuilder.py +134 -0
- sqlobject/tests/test_sqlbuilder_dbspecific.py +66 -0
- sqlobject/tests/test_sqlbuilder_importproxy.py +55 -0
- sqlobject/tests/test_sqlbuilder_joins_instances.py +90 -0
- sqlobject/tests/test_sqlite.py +160 -0
- sqlobject/tests/test_sqlmeta_idName.py +52 -0
- sqlobject/tests/test_sqlobject_admin.py +38 -0
- sqlobject/tests/test_string_id.py +79 -0
- sqlobject/tests/test_style.py +36 -0
- sqlobject/tests/test_subqueries.py +142 -0
- sqlobject/tests/test_transactions.py +122 -0
- sqlobject/tests/test_unicode.py +84 -0
- sqlobject/tests/test_uuidcol.py +40 -0
- sqlobject/tests/test_validation.py +125 -0
- sqlobject/tests/test_views.py +159 -0
- sqlobject/util/__init__.py +1 -0
- sqlobject/util/csvexport.py +210 -0
- sqlobject/util/csvimport.py +366 -0
- sqlobject/util/moduleloader.py +35 -0
- sqlobject/util/threadinglocal.py +6 -0
- sqlobject/versioning/__init__.py +120 -0
- sqlobject/versioning/test/__init__.py +0 -0
- sqlobject/versioning/test/test_version.py +183 -0
- sqlobject/views.py +151 -0
- sqlobject/wsgi_middleware.py +109 -0
- sqlobject-3.13.1.data/scripts/sqlobject-admin +36 -0
- sqlobject-3.13.1.data/scripts/sqlobject-convertOldURI +19 -0
- sqlobject-3.13.1.dist-info/METADATA +177 -0
- sqlobject-3.13.1.dist-info/RECORD +149 -0
- sqlobject-3.13.1.dist-info/WHEEL +6 -0
- sqlobject-3.13.1.dist-info/entry_points.txt +4 -0
- sqlobject-3.13.1.dist-info/top_level.txt +1 -0
sqlobject/sresults.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from . import dbconnection
|
|
2
|
+
from . import sqlbuilder
|
|
3
|
+
from .compat import string_type
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = ['SelectResults']
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SelectResults(object):
|
|
10
|
+
IterationClass = dbconnection.Iteration
|
|
11
|
+
|
|
12
|
+
def __init__(self, sourceClass, clause, clauseTables=None,
|
|
13
|
+
**ops):
|
|
14
|
+
self.sourceClass = sourceClass
|
|
15
|
+
if clause is None or isinstance(clause, str) and clause == 'all':
|
|
16
|
+
clause = sqlbuilder.SQLTrueClause
|
|
17
|
+
if not isinstance(clause, sqlbuilder.SQLExpression):
|
|
18
|
+
clause = sqlbuilder.SQLConstant(clause)
|
|
19
|
+
self.clause = clause
|
|
20
|
+
self.ops = ops
|
|
21
|
+
if ops.get('orderBy', sqlbuilder.NoDefault) is sqlbuilder.NoDefault:
|
|
22
|
+
ops['orderBy'] = sourceClass.sqlmeta.defaultOrder
|
|
23
|
+
orderBy = ops['orderBy']
|
|
24
|
+
if isinstance(orderBy, (tuple, list)):
|
|
25
|
+
orderBy = list(map(self._mungeOrderBy, orderBy))
|
|
26
|
+
else:
|
|
27
|
+
orderBy = self._mungeOrderBy(orderBy)
|
|
28
|
+
ops['dbOrderBy'] = orderBy
|
|
29
|
+
if 'connection' in ops and ops['connection'] is None:
|
|
30
|
+
del ops['connection']
|
|
31
|
+
if ops.get('limit', None):
|
|
32
|
+
assert not ops.get('start', None) and not ops.get('end', None), \
|
|
33
|
+
"'limit' cannot be used with 'start' or 'end'"
|
|
34
|
+
ops["start"] = 0
|
|
35
|
+
ops["end"] = ops.pop("limit")
|
|
36
|
+
|
|
37
|
+
tablesSet = sqlbuilder.tablesUsedSet(self.clause,
|
|
38
|
+
self._getConnection().dbName)
|
|
39
|
+
if clauseTables:
|
|
40
|
+
for table in clauseTables:
|
|
41
|
+
tablesSet.add(table)
|
|
42
|
+
self.clauseTables = clauseTables
|
|
43
|
+
# Explicitly post-adding-in sqlmeta.table,
|
|
44
|
+
# sqlbuilder.Select will handle sqlrepr'ing and dupes.
|
|
45
|
+
self.tables = list(tablesSet) + [sourceClass.sqlmeta.table]
|
|
46
|
+
|
|
47
|
+
def queryForSelect(self):
|
|
48
|
+
columns = [self.sourceClass.q.id] + \
|
|
49
|
+
[getattr(self.sourceClass.q, x.name)
|
|
50
|
+
for x in self.sourceClass.sqlmeta.columnList]
|
|
51
|
+
query = sqlbuilder.Select(columns,
|
|
52
|
+
where=self.clause,
|
|
53
|
+
join=self.ops.get(
|
|
54
|
+
'join', sqlbuilder.NoDefault),
|
|
55
|
+
distinct=self.ops.get('distinct', False),
|
|
56
|
+
lazyColumns=self.ops.get(
|
|
57
|
+
'lazyColumns', False),
|
|
58
|
+
start=self.ops.get('start', 0),
|
|
59
|
+
end=self.ops.get('end', None),
|
|
60
|
+
orderBy=self.ops.get(
|
|
61
|
+
'dbOrderBy', sqlbuilder.NoDefault),
|
|
62
|
+
reversed=self.ops.get('reversed', False),
|
|
63
|
+
staticTables=self.tables,
|
|
64
|
+
forUpdate=self.ops.get('forUpdate', False))
|
|
65
|
+
return query
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
return "<%s at %x>" % (self.__class__.__name__, id(self))
|
|
69
|
+
|
|
70
|
+
def _getConnection(self):
|
|
71
|
+
return self.ops.get('connection') or self.sourceClass._connection
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
conn = self._getConnection()
|
|
75
|
+
return conn.queryForSelect(self)
|
|
76
|
+
|
|
77
|
+
def _mungeOrderBy(self, orderBy):
|
|
78
|
+
if isinstance(orderBy, string_type) and orderBy.startswith('-'):
|
|
79
|
+
orderBy = orderBy[1:]
|
|
80
|
+
desc = True
|
|
81
|
+
else:
|
|
82
|
+
desc = False
|
|
83
|
+
if isinstance(orderBy, string_type):
|
|
84
|
+
if orderBy in self.sourceClass.sqlmeta.columns:
|
|
85
|
+
val = getattr(self.sourceClass.q,
|
|
86
|
+
self.sourceClass.sqlmeta.columns[orderBy].name)
|
|
87
|
+
if desc:
|
|
88
|
+
return sqlbuilder.DESC(val)
|
|
89
|
+
else:
|
|
90
|
+
return val
|
|
91
|
+
else:
|
|
92
|
+
orderBy = sqlbuilder.SQLConstant(orderBy)
|
|
93
|
+
if desc:
|
|
94
|
+
return sqlbuilder.DESC(orderBy)
|
|
95
|
+
else:
|
|
96
|
+
return orderBy
|
|
97
|
+
else:
|
|
98
|
+
return orderBy
|
|
99
|
+
|
|
100
|
+
def clone(self, **newOps):
|
|
101
|
+
ops = self.ops.copy()
|
|
102
|
+
ops.update(newOps)
|
|
103
|
+
return self.__class__(self.sourceClass, self.clause,
|
|
104
|
+
self.clauseTables, **ops)
|
|
105
|
+
|
|
106
|
+
def orderBy(self, orderBy):
|
|
107
|
+
return self.clone(orderBy=orderBy)
|
|
108
|
+
|
|
109
|
+
def connection(self, conn):
|
|
110
|
+
return self.clone(connection=conn)
|
|
111
|
+
|
|
112
|
+
def limit(self, limit):
|
|
113
|
+
return self[:limit]
|
|
114
|
+
|
|
115
|
+
def lazyColumns(self, value):
|
|
116
|
+
return self.clone(lazyColumns=value)
|
|
117
|
+
|
|
118
|
+
def reversed(self):
|
|
119
|
+
return self.clone(reversed=not self.ops.get('reversed', False))
|
|
120
|
+
|
|
121
|
+
def distinct(self):
|
|
122
|
+
return self.clone(distinct=True)
|
|
123
|
+
|
|
124
|
+
def newClause(self, new_clause):
|
|
125
|
+
return self.__class__(self.sourceClass, new_clause,
|
|
126
|
+
self.clauseTables, **self.ops)
|
|
127
|
+
|
|
128
|
+
def filter(self, filter_clause):
|
|
129
|
+
if filter_clause is None:
|
|
130
|
+
# None doesn't filter anything, it's just a no-op:
|
|
131
|
+
return self
|
|
132
|
+
clause = self.clause
|
|
133
|
+
if isinstance(clause, string_type):
|
|
134
|
+
clause = sqlbuilder.SQLConstant('(%s)' % clause)
|
|
135
|
+
return self.newClause(sqlbuilder.AND(clause, filter_clause))
|
|
136
|
+
|
|
137
|
+
def __getitem__(self, value):
|
|
138
|
+
if isinstance(value, slice):
|
|
139
|
+
assert not value.step, "Slices do not support steps"
|
|
140
|
+
if not value.start and not value.stop:
|
|
141
|
+
# No need to copy, I'm immutable
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
# Negative indexes aren't handled (and everything we
|
|
145
|
+
# don't handle ourselves we just create a list to
|
|
146
|
+
# handle)
|
|
147
|
+
if (value.start and value.start < 0) \
|
|
148
|
+
or (value.stop and value.stop < 0):
|
|
149
|
+
if value.start:
|
|
150
|
+
if value.stop:
|
|
151
|
+
return list(self)[value.start:value.stop]
|
|
152
|
+
return list(self)[value.start:]
|
|
153
|
+
return list(self)[:value.stop]
|
|
154
|
+
|
|
155
|
+
if value.start:
|
|
156
|
+
assert value.start >= 0
|
|
157
|
+
start = self.ops.get('start', 0) + value.start
|
|
158
|
+
if value.stop is not None:
|
|
159
|
+
assert value.stop >= 0
|
|
160
|
+
if value.stop < value.start:
|
|
161
|
+
# an empty result:
|
|
162
|
+
end = start
|
|
163
|
+
else:
|
|
164
|
+
end = value.stop + self.ops.get('start', 0)
|
|
165
|
+
if self.ops.get('end', None) is not None and \
|
|
166
|
+
self.ops['end'] < end:
|
|
167
|
+
# truncated by previous slice:
|
|
168
|
+
end = self.ops['end']
|
|
169
|
+
else:
|
|
170
|
+
end = self.ops.get('end', None)
|
|
171
|
+
else:
|
|
172
|
+
start = self.ops.get('start', 0)
|
|
173
|
+
end = value.stop + start
|
|
174
|
+
if self.ops.get('end', None) is not None \
|
|
175
|
+
and self.ops['end'] < end:
|
|
176
|
+
end = self.ops['end']
|
|
177
|
+
return self.clone(start=start, end=end)
|
|
178
|
+
else:
|
|
179
|
+
if value < 0:
|
|
180
|
+
return list(iter(self))[value]
|
|
181
|
+
else:
|
|
182
|
+
start = self.ops.get('start', 0) + value
|
|
183
|
+
return list(self.clone(start=start, end=start + 1))[0]
|
|
184
|
+
|
|
185
|
+
def __iter__(self):
|
|
186
|
+
# @@: This could be optimized, using a simpler algorithm
|
|
187
|
+
# since we don't have to worry about garbage collection,
|
|
188
|
+
# etc., like we do with .lazyIter()
|
|
189
|
+
return iter(list(self.lazyIter()))
|
|
190
|
+
|
|
191
|
+
def lazyIter(self):
|
|
192
|
+
"""
|
|
193
|
+
Returns an iterator that will lazily pull rows out of the
|
|
194
|
+
database and return SQLObject instances
|
|
195
|
+
"""
|
|
196
|
+
conn = self._getConnection()
|
|
197
|
+
return conn.iterSelect(self)
|
|
198
|
+
|
|
199
|
+
def accumulate(self, *expressions):
|
|
200
|
+
""" Use accumulate expression(s) to select result
|
|
201
|
+
using another SQL select through current
|
|
202
|
+
connection.
|
|
203
|
+
Return the accumulate result
|
|
204
|
+
"""
|
|
205
|
+
conn = self._getConnection()
|
|
206
|
+
exprs = []
|
|
207
|
+
for expr in expressions:
|
|
208
|
+
if not isinstance(expr, sqlbuilder.SQLExpression):
|
|
209
|
+
expr = sqlbuilder.SQLConstant(expr)
|
|
210
|
+
exprs.append(expr)
|
|
211
|
+
return conn.accumulateSelect(self, *exprs)
|
|
212
|
+
|
|
213
|
+
def count(self):
|
|
214
|
+
""" Counting elements of current select results """
|
|
215
|
+
assert not self.ops.get('start') and not self.ops.get('end'), \
|
|
216
|
+
"start/end/limit have no meaning with 'count'"
|
|
217
|
+
assert not (self.ops.get('distinct')
|
|
218
|
+
and (self.ops.get('start') or self.ops.get('end'))), \
|
|
219
|
+
"distinct-counting of sliced objects is not supported"
|
|
220
|
+
if self.ops.get('distinct'):
|
|
221
|
+
# Column must be specified, so we are using unique ID column.
|
|
222
|
+
# COUNT(DISTINCT column) is supported by MySQL and PostgreSQL,
|
|
223
|
+
# but not by SQLite. Perhaps more portable would be subquery:
|
|
224
|
+
# SELECT COUNT(*) FROM (SELECT DISTINCT id FROM table)
|
|
225
|
+
count = self.accumulate(
|
|
226
|
+
'COUNT(DISTINCT %s)' % self._getConnection().sqlrepr(
|
|
227
|
+
self.sourceClass.q.id))
|
|
228
|
+
else:
|
|
229
|
+
count = self.accumulate('COUNT(*)')
|
|
230
|
+
if self.ops.get('start'):
|
|
231
|
+
count -= self.ops['start']
|
|
232
|
+
if self.ops.get('end'):
|
|
233
|
+
count = min(self.ops['end'] - self.ops.get('start', 0), count)
|
|
234
|
+
return count
|
|
235
|
+
|
|
236
|
+
def accumulateMany(self, *attributes):
|
|
237
|
+
""" Making the expressions for count/sum/min/max/avg
|
|
238
|
+
of a given select result attributes.
|
|
239
|
+
`attributes` must be a list/tuple of pairs (func_name, attribute);
|
|
240
|
+
`attribute` can be a column name (like 'a_column')
|
|
241
|
+
or a dot-q attribute (like Table.q.aColumn)
|
|
242
|
+
"""
|
|
243
|
+
expressions = []
|
|
244
|
+
conn = self._getConnection()
|
|
245
|
+
if self.ops.get('distinct'):
|
|
246
|
+
distinct = 'DISTINCT '
|
|
247
|
+
else:
|
|
248
|
+
distinct = ''
|
|
249
|
+
for func_name, attribute in attributes:
|
|
250
|
+
if not isinstance(attribute, str):
|
|
251
|
+
attribute = conn.sqlrepr(attribute)
|
|
252
|
+
expression = '%s(%s%s)' % (func_name, distinct, attribute)
|
|
253
|
+
expressions.append(expression)
|
|
254
|
+
return self.accumulate(*expressions)
|
|
255
|
+
|
|
256
|
+
def accumulateOne(self, func_name, attribute):
|
|
257
|
+
""" Making the sum/min/max/avg of a given select result attribute.
|
|
258
|
+
`attribute` can be a column name (like 'a_column')
|
|
259
|
+
or a dot-q attribute (like Table.q.aColumn)
|
|
260
|
+
"""
|
|
261
|
+
return self.accumulateMany((func_name, attribute))
|
|
262
|
+
|
|
263
|
+
def sum(self, attribute):
|
|
264
|
+
return self.accumulateOne("SUM", attribute)
|
|
265
|
+
|
|
266
|
+
def min(self, attribute):
|
|
267
|
+
return self.accumulateOne("MIN", attribute)
|
|
268
|
+
|
|
269
|
+
def avg(self, attribute):
|
|
270
|
+
return self.accumulateOne("AVG", attribute)
|
|
271
|
+
|
|
272
|
+
def max(self, attribute):
|
|
273
|
+
return self.accumulateOne("MAX", attribute)
|
|
274
|
+
|
|
275
|
+
def getOne(self, default=sqlbuilder.NoDefault):
|
|
276
|
+
"""
|
|
277
|
+
If a query is expected to only return a single value,
|
|
278
|
+
using ``.getOne()`` will return just that value.
|
|
279
|
+
|
|
280
|
+
If not results are found, ``SQLObjectNotFound`` will be
|
|
281
|
+
raised, unless you pass in a default value (like
|
|
282
|
+
``.getOne(None)``).
|
|
283
|
+
|
|
284
|
+
If more than one result is returned,
|
|
285
|
+
``SQLObjectIntegrityError`` will be raised.
|
|
286
|
+
"""
|
|
287
|
+
from . import main
|
|
288
|
+
results = list(self)
|
|
289
|
+
if not results:
|
|
290
|
+
if default is sqlbuilder.NoDefault:
|
|
291
|
+
raise main.SQLObjectNotFound(
|
|
292
|
+
"No results matched the query for %s"
|
|
293
|
+
% self.sourceClass.__name__)
|
|
294
|
+
return default
|
|
295
|
+
if len(results) > 1:
|
|
296
|
+
raise main.SQLObjectIntegrityError(
|
|
297
|
+
"More than one result returned from query: %s"
|
|
298
|
+
% results)
|
|
299
|
+
return results[0]
|
|
300
|
+
|
|
301
|
+
def throughTo(self):
|
|
302
|
+
class _throughTo_getter(object):
|
|
303
|
+
def __init__(self, inst):
|
|
304
|
+
self.sresult = inst
|
|
305
|
+
|
|
306
|
+
def __getattr__(self, attr):
|
|
307
|
+
return self.sresult._throughTo(attr)
|
|
308
|
+
return _throughTo_getter(self)
|
|
309
|
+
throughTo = property(throughTo)
|
|
310
|
+
|
|
311
|
+
def _throughTo(self, attr):
|
|
312
|
+
otherClass = None
|
|
313
|
+
orderBy = sqlbuilder.NoDefault
|
|
314
|
+
|
|
315
|
+
ref = self.sourceClass.sqlmeta.columns.get(
|
|
316
|
+
attr.endswith('ID') and attr or attr + 'ID', None)
|
|
317
|
+
if ref and ref.foreignKey:
|
|
318
|
+
otherClass, clause = self._throughToFK(ref)
|
|
319
|
+
else:
|
|
320
|
+
join = [x for x in self.sourceClass.sqlmeta.joins
|
|
321
|
+
if x.joinMethodName == attr]
|
|
322
|
+
if join:
|
|
323
|
+
join = join[0]
|
|
324
|
+
orderBy = join.orderBy
|
|
325
|
+
if hasattr(join, 'otherColumn'):
|
|
326
|
+
otherClass, clause = self._throughToRelatedJoin(join)
|
|
327
|
+
else:
|
|
328
|
+
otherClass, clause = self._throughToMultipleJoin(join)
|
|
329
|
+
|
|
330
|
+
if not otherClass:
|
|
331
|
+
raise AttributeError(
|
|
332
|
+
"throughTo argument (got %s) should be "
|
|
333
|
+
"name of foreignKey or SQL*Join in %s" % (attr,
|
|
334
|
+
self.sourceClass))
|
|
335
|
+
|
|
336
|
+
return otherClass.select(clause,
|
|
337
|
+
orderBy=orderBy,
|
|
338
|
+
connection=self._getConnection())
|
|
339
|
+
|
|
340
|
+
def _throughToFK(self, col):
|
|
341
|
+
otherClass = getattr(self.sourceClass, "_SO_class_" + col.foreignKey)
|
|
342
|
+
colName = col.name
|
|
343
|
+
query = self.queryForSelect().newItems([
|
|
344
|
+
sqlbuilder.ColumnAS(getattr(self.sourceClass.q, colName), colName)
|
|
345
|
+
]).orderBy(None).distinct()
|
|
346
|
+
query = sqlbuilder.Alias(query,
|
|
347
|
+
"%s_%s" % (self.sourceClass.__name__,
|
|
348
|
+
col.name))
|
|
349
|
+
return otherClass, otherClass.q.id == getattr(query.q, colName)
|
|
350
|
+
|
|
351
|
+
def _throughToMultipleJoin(self, join):
|
|
352
|
+
otherClass = join.otherClass
|
|
353
|
+
colName = join.soClass.sqlmeta.style.\
|
|
354
|
+
dbColumnToPythonAttr(join.joinColumn)
|
|
355
|
+
query = self.queryForSelect().newItems(
|
|
356
|
+
[sqlbuilder.ColumnAS(self.sourceClass.q.id, 'id')]).\
|
|
357
|
+
orderBy(None).distinct()
|
|
358
|
+
query = sqlbuilder.Alias(query,
|
|
359
|
+
"%s_%s" % (self.sourceClass.__name__,
|
|
360
|
+
join.joinMethodName))
|
|
361
|
+
joinColumn = getattr(otherClass.q, colName)
|
|
362
|
+
return otherClass, joinColumn == query.q.id
|
|
363
|
+
|
|
364
|
+
def _throughToRelatedJoin(self, join):
|
|
365
|
+
otherClass = join.otherClass
|
|
366
|
+
intTable = sqlbuilder.Table(join.intermediateTable)
|
|
367
|
+
colName = join.joinColumn
|
|
368
|
+
query = self.queryForSelect().newItems(
|
|
369
|
+
[sqlbuilder.ColumnAS(self.sourceClass.q.id, 'id')]).\
|
|
370
|
+
orderBy(None).distinct()
|
|
371
|
+
query = sqlbuilder.Alias(query,
|
|
372
|
+
"%s_%s" % (self.sourceClass.__name__,
|
|
373
|
+
join.joinMethodName))
|
|
374
|
+
clause = sqlbuilder.AND(
|
|
375
|
+
otherClass.q.id == getattr(intTable, join.otherColumn),
|
|
376
|
+
getattr(intTable, colName) == query.q.id)
|
|
377
|
+
return otherClass, clause
|
sqlobject/styles.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = ["Style", "MixedCaseUnderscoreStyle", "DefaultStyle",
|
|
5
|
+
"MixedCaseStyle"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Style(object):
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
The base Style class, and also the simplest implementation. No
|
|
12
|
+
translation occurs -- column names and attribute names match,
|
|
13
|
+
as do class names and table names (when using auto class or
|
|
14
|
+
schema generation).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, pythonAttrToDBColumn=None,
|
|
18
|
+
dbColumnToPythonAttr=None,
|
|
19
|
+
pythonClassToDBTable=None,
|
|
20
|
+
dbTableToPythonClass=None,
|
|
21
|
+
idForTable=None,
|
|
22
|
+
longID=False):
|
|
23
|
+
if pythonAttrToDBColumn:
|
|
24
|
+
self.pythonAttrToDBColumn = \
|
|
25
|
+
lambda a, s=self: pythonAttrToDBColumn(s, a)
|
|
26
|
+
if dbColumnToPythonAttr:
|
|
27
|
+
self.dbColumnToPythonAttr = \
|
|
28
|
+
lambda a, s=self: dbColumnToPythonAttr(s, a)
|
|
29
|
+
if pythonClassToDBTable:
|
|
30
|
+
self.pythonClassToDBTable = \
|
|
31
|
+
lambda a, s=self: pythonClassToDBTable(s, a)
|
|
32
|
+
if dbTableToPythonClass:
|
|
33
|
+
self.dbTableToPythonClass = \
|
|
34
|
+
lambda a, s=self: dbTableToPythonClass(s, a)
|
|
35
|
+
if idForTable:
|
|
36
|
+
self.idForTable = lambda a, s=self: idForTable(s, a)
|
|
37
|
+
self.longID = longID
|
|
38
|
+
|
|
39
|
+
def pythonAttrToDBColumn(self, attr):
|
|
40
|
+
return attr
|
|
41
|
+
|
|
42
|
+
def dbColumnToPythonAttr(self, col):
|
|
43
|
+
return col
|
|
44
|
+
|
|
45
|
+
def pythonClassToDBTable(self, className):
|
|
46
|
+
return className
|
|
47
|
+
|
|
48
|
+
def dbTableToPythonClass(self, table):
|
|
49
|
+
return table
|
|
50
|
+
|
|
51
|
+
def idForTable(self, table):
|
|
52
|
+
if self.longID:
|
|
53
|
+
return self.tableReference(table)
|
|
54
|
+
else:
|
|
55
|
+
return 'id'
|
|
56
|
+
|
|
57
|
+
def pythonClassToAttr(self, className):
|
|
58
|
+
return lowerword(className)
|
|
59
|
+
|
|
60
|
+
def instanceAttrToIDAttr(self, attr):
|
|
61
|
+
return attr + "ID"
|
|
62
|
+
|
|
63
|
+
def instanceIDAttrToAttr(self, attr):
|
|
64
|
+
return attr[:-2]
|
|
65
|
+
|
|
66
|
+
def tableReference(self, table):
|
|
67
|
+
return table + "_id"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MixedCaseUnderscoreStyle(Style):
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
This is the default style. Python attributes use mixedCase,
|
|
74
|
+
while database columns use underscore_separated.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def pythonAttrToDBColumn(self, attr):
|
|
78
|
+
return mixedToUnder(attr)
|
|
79
|
+
|
|
80
|
+
def dbColumnToPythonAttr(self, col):
|
|
81
|
+
return underToMixed(col)
|
|
82
|
+
|
|
83
|
+
def pythonClassToDBTable(self, className):
|
|
84
|
+
return className[0].lower() \
|
|
85
|
+
+ mixedToUnder(className[1:])
|
|
86
|
+
|
|
87
|
+
def dbTableToPythonClass(self, table):
|
|
88
|
+
return table[0].upper() \
|
|
89
|
+
+ underToMixed(table[1:])
|
|
90
|
+
|
|
91
|
+
def pythonClassToDBTableReference(self, className):
|
|
92
|
+
return self.tableReference(self.pythonClassToDBTable(className))
|
|
93
|
+
|
|
94
|
+
def tableReference(self, table):
|
|
95
|
+
return table + "_id"
|
|
96
|
+
|
|
97
|
+
DefaultStyle = MixedCaseUnderscoreStyle
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MixedCaseStyle(Style):
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
This style leaves columns as mixed-case, and uses long
|
|
104
|
+
ID names (like ProductID instead of simply id).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def pythonAttrToDBColumn(self, attr):
|
|
108
|
+
return capword(attr)
|
|
109
|
+
|
|
110
|
+
def dbColumnToPythonAttr(self, col):
|
|
111
|
+
return lowerword(col)
|
|
112
|
+
|
|
113
|
+
def dbTableToPythonClass(self, table):
|
|
114
|
+
return capword(table)
|
|
115
|
+
|
|
116
|
+
def tableReference(self, table):
|
|
117
|
+
return table + "ID"
|
|
118
|
+
|
|
119
|
+
defaultStyle = DefaultStyle()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def getStyle(soClass, dbConnection=None):
|
|
123
|
+
if dbConnection is None:
|
|
124
|
+
if hasattr(soClass, '_connection'):
|
|
125
|
+
dbConnection = soClass._connection
|
|
126
|
+
if hasattr(soClass.sqlmeta, 'style') and soClass.sqlmeta.style:
|
|
127
|
+
return soClass.sqlmeta.style
|
|
128
|
+
elif dbConnection and dbConnection.style:
|
|
129
|
+
return dbConnection.style
|
|
130
|
+
else:
|
|
131
|
+
return defaultStyle
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
############################################################
|
|
135
|
+
# Text utilities
|
|
136
|
+
############################################################
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_mixedToUnderRE = re.compile(r'[A-Z]+')
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def mixedToUnder(s):
|
|
143
|
+
if s.endswith('ID'):
|
|
144
|
+
return mixedToUnder(s[:-2] + "_id")
|
|
145
|
+
trans = _mixedToUnderRE.sub(mixedToUnderSub, s)
|
|
146
|
+
if trans.startswith('_'):
|
|
147
|
+
trans = trans[1:]
|
|
148
|
+
return trans
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def mixedToUnderSub(match):
|
|
152
|
+
m = match.group(0).lower()
|
|
153
|
+
if len(m) > 1:
|
|
154
|
+
return '_%s_%s' % (m[:-1], m[-1])
|
|
155
|
+
else:
|
|
156
|
+
return '_%s' % m
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def capword(s):
|
|
160
|
+
return s[0].upper() + s[1:]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def lowerword(s):
|
|
164
|
+
return s[0].lower() + s[1:]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_underToMixedRE = re.compile('_.')
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def underToMixed(name):
|
|
171
|
+
if name.endswith('_id'):
|
|
172
|
+
return underToMixed(name[:-3] + "ID")
|
|
173
|
+
return _underToMixedRE.sub(lambda m: m.group(0)[1].upper(),
|
|
174
|
+
name)
|