-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathSQLObjectStore.py
More file actions
1104 lines (900 loc) · 39 KB
/
SQLObjectStore.py
File metadata and controls
1104 lines (900 loc) · 39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
from MiddleObject import MiddleObject
from ObjectStore import ObjectStore, UnknownObjectError
from ObjectKey import ObjectKey
from MiddleKit.Core.ObjRefAttr import objRefJoin, objRefSplit
from MiscUtils import NoDefault, AbstractError, CSVJoiner
from MiscUtils import Funcs as funcs
from MiscUtils.DBPool import DBPool
from MiscUtils.MixIn import MixIn
class SQLObjectStoreError(Exception):
"""SQL object store error"""
class SQLObjectStoreThreadingError(SQLObjectStoreError):
"""SQL object store threading error"""
class ObjRefError(SQLObjectStoreError):
"""SQL object store object reference error"""
class ObjRefZeroSerialNumError(ObjRefError):
"""SQL object store serial number zero error"""
class ObjRefDanglesError(ObjRefError):
"""SQL object store object dangles error"""
aggressiveGC = False
class UnknownSerialNumberError(SQLObjectStoreError):
"""For internal use when archiving objects.
Sometimes an obj ref cannot be immediately resolved on INSERT because
the target has not yet been inserted and therefore, given a serial number.
"""
def __init__(self, info):
self.info = info
def __repr__(self):
return '%s: %s' % (self.__class__.__name__, self.info)
def __str__(self):
return str(self.info)
class UnknownSerialNumInfo(object):
"""For internal use when archiving objects.
Attrs assigned externally are:
sourceObject
sourceAttr
targetObject
"""
def updateStmt(self):
assert self.sourceObject.serialNum() != 0
assert self.targetObject.serialNum() != 0
sourceKlass = self.sourceObject._mk_klass
assert sourceKlass
sourceTableName = sourceKlass.sqlTableName()
sourceSqlSerialName = sourceKlass.sqlSerialColumnName()
return 'update %s set %s where %s=%s;' % (
sourceTableName, self.sourceAttr.sqlUpdateExpr(self.targetObject),
sourceSqlSerialName, self.sourceObject.serialNum())
def __repr__(self):
s = []
for item in self.__dict__.items():
s.append('%s=%r' % item)
s = ' '.join(s)
return '<%s %s>' % (self.__class__.__name__, s)
class SQLObjectStore(ObjectStore):
"""The MiddleKit SQL Object Store.
TO DO:
* _sqlEcho should be accessible via a config file setting
as stdout, stderr or a filename.
For details on DB API 2.0, including the thread safety levels see:
http://www.python.org/topics/database/DatabaseAPI-2.0.html
"""
## Init ##
def __init__(self, **kwargs):
# @@ 2001-02-12 ce: We probably need a dictionary before kwargs
# for subclasses to pass to us in case they override __init__()
# and end up collecting kwargs themselves
ObjectStore.__init__(self)
self._dbArgs = kwargs
self._connected = False
self._commited = False
self._sqlEcho = None
self._sqlCount = 0
self._pool = None # an optional DBPool
def modelWasSet(self):
"""Perform additional set up of the store after the model is set.
Performs additional set up of the store after the model is set, normally
via setModel() or readModelFileNamed(). This includes checking that
threading conditions are valid, and connecting to the database.
"""
ObjectStore.modelWasSet(self)
# Check thread safety
self._threadSafety = self.threadSafety()
if self._threaded and self._threadSafety == 0:
raise SQLObjectStoreThreadingError('Threaded is True,'
' but the DB API threadsafety is 0.')
# Cache some settings
self._markDeletes = self.setting('DeleteBehavior', 'delete') == 'mark'
# Set up SQL echo
self.setUpSQLEcho()
# Set up attrs for caching
for klass in self.model().allKlassesInOrder():
klass._getMethods = {}
klass._setMethods = {}
for attr in klass.allDataAttrs():
attr._sqlColumnName = None
attr._sqlColumnNames = None
# use dbargs from settings file as defaults
# (args passed to __init__ take precedence)
args = self._dbArgs
self._dbArgs = self.setting('DatabaseArgs', {})
self._dbArgs.update(args)
# print 'dbArgs = %s' % self._dbArgs
# Connect
self.connect()
def setUpSQLEcho(self):
"""Set up the SQL echoing/logging for the store.
The logging is set up according to the setting 'SQLLog'.
See the User's Guide for more info. Invoked by modelWasSet().
"""
setting = self.setting('SQLLog', None)
if setting is None or setting == {}:
self._sqlEcho = None
else:
filename = setting['File']
if filename is None:
self._sqlEcho = None
elif filename == 'stdout':
self._sqlEcho = sys.stdout
elif filename == 'stderr':
self._sqlEcho = sys.stderr
else:
mode = setting.get('Mode', 'write')
assert mode in ['write', 'append']
mode = mode[0]
self._sqlEcho = open(filename, mode)
## Connecting to the db ##
def isConnected(self):
return self._connected
def connect(self):
"""Connect to the database.
Connects to the database only if the store has not already and provided
that the store has a valid model.
The default implementation of connect() is usually sufficient provided
that subclasses have implemented newConnection().
"""
assert self._model, 'Cannot connect:' \
' No model has been attached to this store yet.'
if not self._connected:
self._connection = self.newConnection()
self._connected = True
self.readKlassIds()
poolSize = self.setting('SQLConnectionPoolSize', 0)
if poolSize:
args = self._dbArgs.copy()
self.augmentDatabaseArgs(args, pool=True)
try:
self._pool = DBPool(self.dbapiModule(), poolSize, **args)
except TypeError:
if 'database' in args:
del args['database']
self._pool = DBPool(self.dbapiModule(), poolSize, **args)
else:
raise
def augmentDatabaseArgs(self, args, pool=False):
# give subclasses the opportunity to add or change
# database arguments
pass
def newConnection(self):
"""Return a DB API 2.0 connection.
This is a utility method invoked by connect(). Subclasses should
implement this, making use of self._dbArgs (a dictionary specifying
host, username, etc.) as well as self._model.sqlDatabaseName().
Subclass responsibility.
"""
raise AbstractError(self.__class__)
def readKlassIds(self):
"""Read the klass ids from the SQL database. Invoked by connect()."""
conn, cur = self.executeSQL('select id, name from _MKClassIds;')
try:
klassesById = {}
for klassId, name in cur.fetchall():
assert klassId, 'Id must be a non-zero int.' \
' id=%r, name=%r' % (klassId, name)
try:
klass = self._model.klass(name)
except KeyError:
filename = self._model.filename()
msg = ('%s The database has a class id for %r in the'
' _MKClassIds table, but no such class exists in'
' the model %s. The model and the db are not in sync.'
% (name, name, filename))
raise KeyError(msg)
klassesById[klassId] = klass
klass.setId(klassId)
finally:
self.doneWithConnection(conn)
self._klassesById = klassesById
## Changes ##
def commitInserts(self, allThreads=False):
unknownSerialNums = []
# @@ ... sort here for dependency order
for obj in self._newObjects.items(allThreads):
self._insertObject(obj, unknownSerialNums)
conn = None
try:
for unknownInfo in unknownSerialNums:
stmt = unknownInfo.updateStmt()
conn, cur = self.executeSQL(stmt, conn)
finally:
self.doneWithConnection(conn)
self._newObjects.clear(allThreads)
def _insertObject(self, obj, unknownSerialNums):
# New objects not in the persistent store have serial numbers less than 1
if obj.serialNum() > 0:
try:
rep = repr(obj)
except Exception:
rep = '(repr exception)'
assert obj.serialNum() < 1, 'obj=%s' % rep
# try to get the next ID (if database supports this)
idNum = self.retrieveNextInsertId(obj.klass())
# SQL insert
sql = obj.sqlInsertStmt(unknownSerialNums, idNum)
conn, cur = self.executeSQL(sql)
try:
# Get new id/serial num
if idNum is None:
idNum = self.retrieveLastInsertId(conn, cur)
# Update object
obj.setSerialNum(idNum)
obj.setKey(ObjectKey().initFromObject(obj))
obj.setChanged(False)
# Update our object pool
self._objects[obj.key()] = obj
finally:
self.doneWithConnection(conn)
def retrieveNextInsertId(self, klass):
"""Return the id for the next new object of this class.
Databases which cannot determine the id until the object has been
added return None, signifying that retrieveLastInsertId
should be called to get the id after the insert has been made.
"""
return None
def retrieveLastInsertId(self, conn, cur):
"""Return the id of the last INSERT operation by this connection.
This id is typically a 32-bit int. Used by commitInserts() to get
the correct serial number for the last inserted object.
"""
return cur.lastrowid
def commitUpdates(self, allThreads=False):
conn = None
try:
for obj in self._changedObjects.values(allThreads):
sql = obj.sqlUpdateStmt()
conn, cur = self.executeSQL(sql, conn)
obj.setChanged(False)
finally:
self.doneWithConnection(conn)
self._changedObjects.clear(allThreads)
def commitDeletions(self, allThreads=False):
conn = None
try:
for obj in self._deletedObjects.items(allThreads):
sql = obj.sqlDeleteStmt()
conn, cur = self.executeSQL(sql, conn)
conn.commit()
finally:
self.doneWithConnection(conn)
self._deletedObjects.clear(allThreads)
## Fetching ##
def fetchObject(self, aClass, serialNum, default=NoDefault):
"""Fetch a single object of a specific class and serial number.
aClass can be a Klass object (from the MiddleKit object model),
the name of the class (e.g., a string) or a Python class.
Raises an exception if aClass parameter is invalid, or the object
cannot be located.
"""
klass = self._klassForClass(aClass)
objects = self.fetchObjectsOfClass(klass, serialNum=serialNum, isDeep=False)
count = len(objects)
if count == 0:
if default is NoDefault:
raise UnknownObjectError('aClass = %r, serialNum = %r'
% (aClass, serialNum))
else:
return default
else:
assert count == 1
return objects[0]
def fetchObjectsOfClass(self, aClass,
clauses='', isDeep=True, refreshAttrs=True, serialNum=None, clausesArgs=None):
"""Fetch a list of objects of a specific class.
The list may be empty if no objects are found.
aClass can be a Klass object (from the MiddleKit object model),
the name of the class (e.g., a string) or a Python class.
The clauses argument can be any SQL clauses such as 'where x<5 order by x'.
Obviously, these could be specific to your SQL database, thereby making
your code non-portable. Use your best judgement.
serialNum can be a specific serial number if you are looking for
a specific object. If serialNum is provided, it overrides the clauses.
You should label all arguments other than aClass:
objs = store.fetchObjectsOfClass('Foo', clauses='where x<5')
The reason for labeling is that this method is likely to undergo
improvements in the future which could include additional arguments.
No guarantees are made about the order of the arguments except that
aClass will always be the first.
Raises an exception if aClass parameter is invalid.
"""
klass = self._klassForClass(aClass)
# Fetch objects of subclasses first, because the code below
# will be modifying clauses and serialNum
deepObjs = []
if isDeep:
for subklass in klass.subklasses():
deepObjs.extend(self.fetchObjectsOfClass(
subklass, clauses, isDeep, refreshAttrs, serialNum, clausesArgs))
# Now get objects of this exact class
objs = []
if not klass.isAbstract():
fetchSQLStart = klass.fetchSQLStart()
className = klass.name()
if serialNum is not None:
serialNum = int(serialNum) # make sure it's a valid int
clauses = 'where %s=%d' % (klass.sqlSerialColumnName(), serialNum)
if self._markDeletes:
clauses = self.addDeletedToClauses(clauses)
conn, cur = self.executeSQL(fetchSQLStart + clauses + ';', clausesArgs=clausesArgs)
try:
for row in cur.fetchall():
serialNum = row[0]
key = ObjectKey().initFromClassNameAndSerialNum(className, serialNum)
obj = self._objects.get(key)
if obj is None:
pyClass = klass.pyClass()
obj = pyClass()
assert isinstance(obj, MiddleObject), (
'Not a MiddleObject. obj = %r, type = %r, MiddleObject = %r'
% (obj, type(obj), MiddleObject))
obj.readStoreData(self, row)
obj.setKey(key)
self._objects[key] = obj
else:
# Existing object
if refreshAttrs:
obj.readStoreData(self, row)
objs.append(obj)
finally:
self.doneWithConnection(conn)
objs.extend(deepObjs)
return objs
def refreshObject(self, obj):
assert obj.store() is self
return self.fetchObject(obj.klass(), obj.serialNum())
## Klasses ##
def klassForId(self, id):
return self._klassesById[id]
## Self utility for SQL, connections, cursors, etc. ##
def executeSQL(self, sql, connection=None, commit=False, clausesArgs=None):
"""Execute the given SQL.
This will connect to the database for the first time if necessary.
This method will also log the SQL to self._sqlEcho, if it is not None.
Returns the connection and cursor used and relies on connectionAndCursor()
to obtain these. Note that you can pass in a connection to force a
particular one to be used and a flag to commit immediately.
"""
sql = str(sql) # Excel-based models yield Unicode strings which some db modules don't like
sql = sql.strip()
if aggressiveGC:
import gc
assert gc.isenabled()
gc.collect()
self._sqlCount += 1
if self._sqlEcho:
timestamp = funcs.timestamp()['pretty']
self._sqlEcho.write('SQL %04i. %s %s\n' % (self._sqlCount, timestamp, sql))
self._sqlEcho.flush()
conn, cur = self.connectionAndCursor(connection)
self._executeSQL(cur, sql, clausesArgs)
if commit:
conn.commit()
return conn, cur
def _executeSQL(self, cur, sql, clausesArgs=None):
"""Invoke execute on the cursor with the given SQL.
This is a hook for subclasses that wish to influence this event.
Invoked by executeSQL().
"""
if clausesArgs:
cur.execute(sql, clausesArgs)
else:
cur.execute(sql)
def executeSQLTransaction(self, transaction, connection=None, commit=True):
"""Execute the given sequence of SQL statements and commit as transaction."""
if isinstance(transaction, basestring):
transaction = [transaction]
try:
for sql in transaction:
if connection:
self.executeSQL(sql, connection)
else:
connection, cur = self.executeSQL(sql)
except Exception:
if connection and commit:
connection.rollback()
raise
if transaction and connection and commit:
try:
connection.commit()
except Exception:
connection.rollback()
raise
return connection
def executeSQLScript(self, sql, connection=None):
"""Execute the given SQL script.
This uses the nonstandard executescript() method as provided
by the PySQLite adapter.
"""
sql = str(sql).strip()
if not connection:
connection = self.newConnection()
if not hasattr(connection, 'executescript'):
raise AttributeError(
'Script execution not supported by database adapter.')
return connection.executescript(sql)
def setSQLEcho(self, file):
"""Set a file to echo sql statements to, as sent through executeSQL().
None can be passed to turn echo off.
"""
self._sqlEcho = file
def connectionAndCursor(self, connection=None):
"""Return the connection and cursor needed for executing SQL.
Takes into account factors such as setting('Threaded') and the
threadsafety level of the DB API module. You can pass in a connection to
force a particular one to be used. Uses newConnection() and connect().
"""
if aggressiveGC:
import gc
assert gc.isenabled()
gc.collect()
if connection:
conn = connection
elif self._threaded:
if self._pool:
conn = self._pool.connection()
elif self._threadSafety == 1:
conn = self.newConnection()
else: # safety = 2, 3
if not self._connected:
self.connect()
conn = self._connection
else:
# Non-threaded
if not self._connected:
self.connect()
conn = self._connection
cursor = conn.cursor()
return conn, cursor
def threadSafety(self):
"""Return the threadsafety of the DB API module."""
return self.dbapiModule().threadsafety
def dbapiVersion(self):
"""Return the version of the DB API module."""
module = self.dbapiModule()
return '%s %s' % (module.__name__, module.version)
def dbVersion(self):
"""Return the database version.
Subclass responsibility.
"""
raise AbstractError(self.__class__)
def addDeletedToClauses(self, clauses):
"""Modify the given set of clauses so that it filters out records with non-NULL deleted field."""
clauses = clauses.strip()
if clauses.lower().startswith('where'):
where = clauses[5:]
orderByIndex = where.lower().find('order by')
if orderByIndex < 0:
orderBy = ''
else:
where, orderBy = where[:orderByIndex], where[orderByIndex:]
return 'where deleted is null and (%s) %s' % (where, orderBy)
else:
return 'where deleted is null %s' % clauses
## Obj refs ##
def fetchObjRef(self, objRef):
"""Fetch referenced object.
Given an unarchived object reference, this method returns the actual
object for it (or None if the reference is NULL or dangling). While
this method assumes that obj refs are stored as 64-bit numbers containing
the class id and object serial number, subclasses are certainly able to
override that assumption by overriding this method.
"""
assert isinstance(objRef, long), 'type=%r, objRef=%r' % (type(objRef), objRef)
if objRef == 0:
return None
else:
klassId, serialNum = objRefSplit(objRef)
if klassId == 0 or serialNum == 0:
# invalid! we don't use 0 serial numbers
return self.objRefZeroSerialNum(objRef)
klass = self.klassForId(klassId)
# Check if we already have this in memory first
key = ObjectKey()
key.initFromClassNameAndSerialNum(klass.name(), serialNum)
obj = self._objects.get(key)
if obj:
return obj
clauses = 'where %s=%d' % (klass.sqlSerialColumnName(), serialNum)
objs = self.fetchObjectsOfClass(klass, clauses, isDeep=False)
if len(objs) == 1:
return objs[0]
elif len(objs) > 1:
# @@ 2000-11-22 ce: expand the msg with more information
raise ValueError('Multiple objects.')
else:
return self.objRefDangles(objRef)
def objRefInMem(self, objRef):
"""Return referenced object in memory.
Returns the object corresponding to the given objref if and only if it
has been loaded into memory. If the object has never been fetched from
the database, None is returned.
"""
assert isinstance(objRef, long), 'type=%r, objRef=%r' % (type(objRef), objRef)
if objRef == 0:
return 0
else:
klassId, serialNum = objRefSplit(objRef)
if klassId == 0 or serialNum == 0:
# invalid! we don't use 0 serial numbers
return self.objRefZeroSerialNum(objRef)
klass = self.klassForId(klassId)
# return whether we have this object in memory
key = ObjectKey()
key.initFromClassNameAndSerialNum(klass.name(), serialNum)
return self._objects.get(key)
def objRefZeroSerialNum(self, objRef):
"""Raise serial number zero error.
Invoked by fetchObjRef() if either the class or the object serial
number is zero.
"""
raise ObjRefZeroSerialNumError(*objRefSplit(objRef))
def objRefDangles(self, objRef):
"""Raise dangling reference error.
Invoked by fetchObjRef() if there is no possible target object
for the given objRef.
E.g., this can happen for a dangling reference. This method invokes
self.warning() and includes the objRef as decimal, hexadecimal
and class:obj numbers.
"""
raise ObjRefDanglesError(*objRefSplit(objRef))
## Special Cases ##
def filterDateTimeDelta(self, dtd):
"""Adapt DateTimeDeltas.
Some databases have no TIME type and therefore will not return
DateTimeDeltas. This utility method is overridden by subclasses
as appropriate and invoked by the test suite.
"""
return dtd
def sqlNowCall(self):
"""Get current DateTime."""
return 'CURRENT_TIMESTAMP'
## Self util ##
def doneWithConnection(self, conn):
"""Invoked by self when a connection is no longer needed.
The default behavior is to commit and close the connection.
"""
if conn is not None:
# Starting with 1.2.0, MySQLdb disables autocommit by default,
# as required by the DB-API standard (PEP-249). If you are using
# InnoDB tables or some other type of transactional table type,
# you'll need to do connection.commit() before closing the connection,
# or else none of your changes will be written to the database.
try:
conn.commit()
except Exception:
pass
conn.close()
## Debugging ##
def dumpTables(self, out=None):
if out is None:
out = sys.stdout
out.write('DUMPING TABLES\n')
out.write('BEGIN\n')
conn = None
try:
for klass in self.model().klasses().values():
out.write(klass.name() + '\n')
conn, cur = self.executeSQL('select * from %s;'
% klass.name(), conn)
out.write(str(self._cursor.fetchall()))
out.write('\n')
finally:
self.doneWithConnection(conn)
out.write('END\n')
def dumpKlassIds(self, out=None):
if out is None:
out = sys.stdout
wr = out.write('DUMPING KLASS IDs\n')
for klass in self.model().klasses().values():
out.write('%25s %2i\n' % (klass.name(), klass.id()))
out.write('\n')
def dumpObjectStore(self, out=None, progress=False):
if out is None:
out = sys.stdout
for klass in sorted(self.model().klasses().values(), key=lambda k: k.name()):
if progress:
sys.stderr.write(".")
out.write('%s objects\n' % (klass.name()))
attrs = [attr for attr in klass.allAttrs() if attr.hasSQLColumn()]
colNames = [attr.name() for attr in attrs]
colNames.insert(0, klass.sqlSerialColumnName())
out.write(CSVJoiner.joinCSVFields(colNames) + "\n")
# write out a line for each object in this class
objlist = self.fetchObjectsOfClass(klass.name(), isDeep=False)
for obj in objlist:
fields = []
fields.append(str(obj.serialNum()))
for attr in attrs:
# jdh 2003-03-07: if the attribute is a dangling object reference, the value
# will be None. This means that dangling references will _not_ be remembered
# across dump/generate/create/insert procedures.
method = getattr(obj, attr.pyGetName())
value = method()
if value is None:
fields.append('')
elif isinstance(value, MiddleObject):
fields.append('%s.%d' % (value.klass().name(),
value.serialNum()))
else:
fields.append(str(value))
out.write(CSVJoiner.joinCSVFields(fields).replace('\r', '\\r'))
out.write('\n')
out.write('\n')
out.write('\n')
if progress:
sys.stderr.write("\n")
class Model(object):
def sqlDatabaseName(self):
"""Return the name of the database.
This is either the 'Database' setting or self.name().
"""
name = self.setting('Database', None)
if name is None:
name = self.name()
return name
class MiddleObjectMixIn(object):
def sqlObjRef(self):
"""Return the 64-bit integer value that refers to self in a SQL database.
This only makes sense if the UseBigIntObjRefColumns setting is True.
"""
return objRefJoin(self.klass().id(), self.serialNum())
def sqlInsertStmt(self, unknowns, id=None):
"""Return SQL insert statements.
Returns the SQL insert statements for MySQL (as a tuple) in the form:
insert into table (name, ...) values (value, ...);
May add an info object to the unknowns list for obj references that
are not yet resolved.
"""
klass = self.klass()
insertSQLStart, sqlAttrs = klass.insertSQLStart(includeSerialColumn=id)
values = []
append = values.append
extend = values.extend
if id is not None:
append(str(id))
for attr in sqlAttrs:
try:
value = attr.sqlValue(self.valueForAttr(attr))
except UnknownSerialNumberError as exc:
exc.info.sourceObject = self
unknowns.append(exc.info)
if self.store().model().setting('UseBigIntObjRefColumns', False):
value = 'NULL'
else:
value = ('NULL', 'NULL')
if isinstance(value, basestring):
append(value)
else:
# value could be sequence for attrs requiring multiple SQL columns
extend(value)
if not values:
values = ['0']
values = ','.join(values)
return insertSQLStart + values + ');'
def sqlUpdateStmt(self):
"""Return SQL update statement.
Returns the SQL update statement of the form:
update table set name=value, ... where idName=idValue;
Installed as a method of MiddleObject.
"""
assert self._mk_changedAttrs
klass = self.klass()
res = []
for attr in self._mk_changedAttrs.values():
res.append(attr.sqlUpdateExpr(self.valueForAttr(attr)))
res = ','.join(res)
res = ('update ', klass.sqlTableName(), ' set ', res, ' where ',
klass.sqlSerialColumnName(), '=', str(self.serialNum()))
return ''.join(res)
def sqlDeleteStmt(self):
"""Return SQL delete statement.
Returns the SQL delete statement for MySQL of the form:
delete from table where idName=idValue;
Or if deletion is being marked with a timestamp:
update table set deleted=Now();
Installed as a method of MiddleObject.
"""
klass = self.klass()
assert klass is not None
if self.store().model().setting('DeleteBehavior', 'delete') == 'mark':
return 'update %s set deleted=%s where %s=%d;' % (
klass.sqlTableName(), self.store().sqlNowCall(),
klass.sqlSerialColumnName(), self.serialNum())
else:
return 'delete from %s where %s=%d;' % (klass.sqlTableName(),
klass.sqlSerialColumnName(), self.serialNum())
def referencingObjectsAndAttrsFetchKeywordArgs(self, backObjRefAttr):
if self.store().setting('UseBigIntObjRefColumns'):
return dict(refreshAttrs=True, clauses='WHERE %s=%s'
% (backObjRefAttr.sqlColumnName(), self.sqlObjRef()))
else:
classIdName, objIdName = backObjRefAttr.sqlColumnNames()
return dict(refreshAttrs=True, clauses='WHERE (%s=%s AND %s=%s)'
% (classIdName, self.klass().id(), objIdName, self.serialNum()))
MixIn(MiddleObject, MiddleObjectMixIn)
# Normally we don't have to invoke MixIn()--it's done automatically.
# However, that only works when augmenting MiddleKit.Core classes
# (MiddleObject belongs to MiddleKit.Run).
import MiddleKit.Design.KlassSQLSerialColumnName
class Klass(object):
_fetchSQLStart = None # help out the caching mechanism in fetchSQLStart()
_insertSQLStart = None # help out the caching mechanism in insertSQLStart()
def sqlTableName(self):
"""Return the name of the SQL table for this class.
Returns self.name().
Subclasses may wish to override to provide special quoting that
prevents name collisions between table names and reserved words.
"""
return self.name()
def fetchSQLStart(self):
if self._fetchSQLStart is None:
attrs = self.allDataAttrs()
attrs = [attr for attr in attrs if attr.hasSQLColumn()]
colNames = [self.sqlSerialColumnName()]
colNames.extend([attr.sqlColumnName() for attr in attrs])
self._fetchSQLStart = 'select %s from %s ' % (','.join(colNames), self.sqlTableName())
return self._fetchSQLStart
def insertSQLStart(self, includeSerialColumn=False):
"""Return a tuple of insertSQLStart (a string) and sqlAttrs (a list)."""
if self._insertSQLStart is None:
res = ['insert into %s (' % self.sqlTableName()]
attrs = self.allDataAttrs()
attrs = [attr for attr in attrs if attr.hasSQLColumn()]
fieldNames = [attr.sqlColumnName() for attr in attrs]
if includeSerialColumn or len(fieldNames) == 0:
fieldNames.insert(0, self.sqlSerialColumnName())
res.append(','.join(fieldNames))
res.append(') values (')
self._insertSQLStart = ''.join(res)
self._sqlAttrs = attrs
return self._insertSQLStart, self._sqlAttrs
class Attr(object):
def shouldRegisterChanges(self):
"""Return self.hasSQLColumn().
This only makes sense since there would be no point in registering
changes on an attribute with no corresponding SQL column. The standard
example of such an attribute is "list".
"""
return self.hasSQLColumn()
def hasSQLColumn(self):
"""Check if there is a correlating SQL column.
Returns true if the attribute has a direct correlating SQL column in
its class' SQL table definition.
Most attributes do. Those of type list do not.
"""
return not self.get('isDerived', False)
def sqlColumnName(self):
"""Return the SQL column name corresponding to this attribute-
This is consisting of self.name() + self.sqlTypeSuffix().
"""
if not self._sqlColumnName:
self._sqlColumnName = self.name()
return self._sqlColumnName
def sqlValue(self, value):
"""Return SQL for Python value.
For a given Python value, this returns the correct string for use in a
SQL statement. Subclasses will typically *not* override this method,
but instead, sqlForNonNone() and on rare occasions, sqlForNone().
"""
if value is None:
return self.sqlForNone()
else:
return self.sqlForNonNone(value)
def sqlForNone(self):
return 'NULL'
def sqlForNonNone(self, value):
return repr(value)
def sqlUpdateExpr(self, value):
"""Return update assignments.
Returns the assignment portion of an UPDATE statement such as:
"foo='bar'"
Using sqlColumnName() and sqlValue(). Subclasses only need to
override this if they have a special need (such as multiple columns,
see ObjRefAttr).
"""
colName = self.sqlColumnName()
return colName + '=' + self.sqlValue(value)
def readStoreDataRow(self, obj, row, i):
"""By default, an attr reads one data value out of the row."""
value = row[i]
obj.setValueForAttr(self, value)
return i + 1
class BasicTypeAttr(object):
pass
class IntAttr(object):
def sqlForNonNone(self, value):
return str(value)
# it's important to use str() since an int might point
# to a long (whose repr() would be suffixed with an 'L')
class LongAttr(object):
def sqlForNonNone(self, value):
return str(value)
class DecimalAttr(object):
def sqlForNonNone(self, value):
return str(value) # repr() will give Decimal("3.4")
class BoolAttr(object):