]> err.no Git - sope/blob - sope-gdl1/MySQL/MySQL4Channel.m
fixed some NGMail framework build issue
[sope] / sope-gdl1 / MySQL / MySQL4Channel.m
1 /* 
2    MySQL4Channel.m
3
4    Copyright (C) 2003-2005 SKYRIX Software AG
5
6    Author: Helge Hess (helge.hess@skyrix.com)
7
8    This file is part of the MySQL4 Adaptor Library
9
10    This library is free software; you can redistribute it and/or
11    modify it under the terms of the GNU Library General Public
12    License as published by the Free Software Foundation; either
13    version 2 of the License, or (at your option) any later version.
14
15    This library is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18    Library General Public License for more details.
19
20    You should have received a copy of the GNU Library General Public
21    License along with this library; see the file COPYING.LIB.
22    If not, write to the Free Software Foundation,
23    59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 */
25
26 #include <ctype.h>
27 #include <string.h>
28 #include <strings.h>
29 #include "MySQL4Channel.h"
30 #include "MySQL4Adaptor.h"
31 #include "MySQL4Exception.h"
32 #include "NSString+MySQL4.h"
33 #include "MySQL4Values.h"
34 #include "EOAttribute+MySQL4.h"
35 #include "common.h"
36 #include <mysql/mysql.h>
37
38 #ifndef MIN
39 #  define MIN(x, y) ((x > y) ? y : x)
40 #endif
41
42 #define MAX_CHAR_BUF 16384
43
44 @implementation MySQL4Channel
45
46 static EONull *null = nil;
47
48 + (void)initialize {
49   if (null == NULL) null = [[EONull null] retain];
50 }
51
52 - (id)initWithAdaptorContext:(EOAdaptorContext*)_adaptorContext {
53   if ((self = [super initWithAdaptorContext:_adaptorContext])) {
54     [self setDebugEnabled:[[NSUserDefaults standardUserDefaults]
55                                            boolForKey:@"MySQL4DebugEnabled"]];
56     
57     self->_attributesForTableName = 
58       [[NSMutableDictionary alloc] initWithCapacity:16];
59     self->_primaryKeysNamesForTableName =
60       [[NSMutableDictionary alloc] initWithCapacity:16];
61   }
62   return self;
63 }
64
65 - (void)_adaptorWillFinalize:(id)_adaptor {
66 }
67
68 - (void)dealloc {
69   if ([self isOpen])
70     [self closeChannel];
71   [self->_attributesForTableName       release];
72   [self->_primaryKeysNamesForTableName release];
73   [super dealloc];
74 }
75
76 /* NSCopying methods */
77
78 - (id)copyWithZone:(NSZone *)zone {
79   return [self retain];
80 }
81
82 // debugging
83
84 - (void)setDebugEnabled:(BOOL)_flag {
85   self->isDebuggingEnabled = _flag;
86 }
87 - (BOOL)isDebugEnabled {
88   return self->isDebuggingEnabled;
89 }
90
91 - (void)receivedMessage:(NSString *)_message {
92   NSLog(@"%@: message %@.", _message);
93 }
94
95 /* open/close */
96
97 static int openConnectionCount = 0;
98
99 - (BOOL)isOpen {
100   return (self->_connection != NULL) ? YES : NO;
101 }
102
103 - (int)maxOpenConnectionCount {
104   static int MaxOpenConnectionCount = -1;
105     
106   if (MaxOpenConnectionCount != -1)
107     return MaxOpenConnectionCount;
108
109   MaxOpenConnectionCount =
110     [[NSUserDefaults standardUserDefaults]
111                      integerForKey:@"MySQL4MaxOpenConnectionCount"];
112   if (MaxOpenConnectionCount == 0)
113     MaxOpenConnectionCount = 150;
114   return MaxOpenConnectionCount;
115 }
116
117 - (BOOL)openChannel {
118   const char *cDBName;
119   MySQL4Adaptor *adaptor;
120   NSString *host, *socket;
121   void *rc;
122   
123   if (self->_connection != NULL) {
124     NSLog(@"%s: Connection already open !!!", __PRETTY_FUNCTION__);
125     return NO;
126   }
127   
128   adaptor = (MySQL4Adaptor *)[adaptorContext adaptor];
129   
130   if (![super openChannel])
131     return NO;
132   
133   if (openConnectionCount > [self maxOpenConnectionCount]) {
134     [MySQL4CouldNotOpenChannelException 
135         raise:@"NoMoreConnections"
136         format:@"cannot open a additional connection !"];
137     return NO;
138   }
139
140   cDBName = [[adaptor databaseName] UTF8String];
141   
142   if ((self->_connection = mysql_init(NULL)) == NULL) {
143     NSLog(@"ERROR(%s): could not allocate MySQL4 connection!");
144     return NO;
145   }
146   
147   // TODO: could change options using mysql_options()
148   
149   host = [adaptor serverName];
150   if ([host hasPrefix:@"/"]) { /* treat hostname as Unix socket path */
151     socket = host;
152     host   = nil;
153   }
154   else
155     socket = nil;
156   
157   rc = mysql_real_connect(self->_connection, 
158                           [host UTF8String],
159                           [[adaptor loginName]     UTF8String],
160                           [[adaptor loginPassword] UTF8String],
161                           cDBName,
162                           [[adaptor port] intValue],
163                           [socket cString],
164                           0);
165   if (rc == NULL) {
166     NSLog(@"ERROR: could not open MySQL4 connection to database '%@': %s",
167           [adaptor databaseName], mysql_error(self->_connection));
168     mysql_close(self->_connection); 
169     self->_connection = NULL;
170     return NO;
171   }
172   
173   if (mysql_query(self->_connection, "SET CHARACTER SET utf8") != 0) {
174     NSLog(@"WARNING(%s): could not put MySQL4 connection into UTF-8 mode: %s",
175           __PRETTY_FUNCTION__, mysql_error(self->_connection));
176 #if 0
177     mysql_close(self->_connection); 
178     self->_connection = NULL;
179     return NO;
180 #endif
181   }
182   
183   if (isDebuggingEnabled)
184     NSLog(@"MySQL4 connection established 0x%08X", self->_connection);
185
186 #if 0
187   NSLog(@"---------- %s: %@ opens channel count[%d]", __PRETTY_FUNCTION__,
188         self, openConnectionCount);
189 #endif
190   openConnectionCount++;
191   
192 #if LIB_FOUNDATION_BOEHM_GC
193   [GarbageCollector registerForFinalizationObserver:self
194                     selector:@selector(_adaptorWillFinalize:)
195                     object:[[self adaptorContext] adaptor]];
196 #endif
197
198   if (isDebuggingEnabled) {
199     NSLog(@"MySQL4 channel 0x%08X opened (connection=0x%08X,%s)",
200           (unsigned)self, self->_connection, cDBName);
201   }
202   return YES;
203 }
204
205 - (void)primaryCloseChannel {
206   if ([self isFetchInProgress])
207     [self cancelFetch];
208   
209   if (self->_connection != NULL) {
210     mysql_close(self->_connection);
211 #if 0
212     NSLog(@"---------- %s: %@ close channel count[%d]", __PRETTY_FUNCTION__,
213           self, openConnectionCount);
214 #endif
215     openConnectionCount--;
216     
217     if (isDebuggingEnabled) {
218       fprintf(stderr, 
219               "MySQL4 connection dropped 0x%08X (channel=0x%08X)\n",
220               (unsigned)self->_connection, (unsigned)self);
221     }
222     self->_connection = NULL;
223   }
224 }
225
226 - (void)closeChannel {
227   [super closeChannel];
228   [self primaryCloseChannel];
229 }
230
231 /* fetching rows */
232
233 - (void)cancelFetch {
234   self->fields = NULL; /* apparently we do not need to free those */
235   
236   if (self->results != NULL) {
237     mysql_free_result(self->results);
238     self->results = NULL;
239   }
240   [super cancelFetch];
241 }
242
243 - (MYSQL_FIELD *)_fetchFields {
244   if (self->results == NULL)
245     return NULL;
246   
247   if (self->fields != NULL)
248     return self->fields;
249   
250   self->fields     = mysql_fetch_fields(self->results);
251   self->fieldCount = mysql_num_fields(self->results);
252   return self->fields;
253 }
254
255 - (NSArray *)describeResults:(BOOL)_beautifyNames {
256   // TODO: make exception-less method
257   MYSQL_FIELD         *mfields;
258   int                 cnt;
259   NSMutableArray      *result    = nil;
260   NSMutableDictionary *usedNames = nil;
261   NSNumber            *yesObj;
262   
263   yesObj = [NSNumber numberWithBool:YES];
264   
265   if (![self isFetchInProgress]) {
266     [MySQL4Exception raise:@"NoFetchInProgress"
267                      format:@"No fetch in progress (channel=%@)", self];
268     return nil;
269   }
270   
271   if ((mfields = [self _fetchFields]) == NULL) {
272     [MySQL4Exception raise:@"NoFieldInfo"
273                      format:@"Failed to fetch field info (channel=%@)", self];
274     return nil;
275   }
276   
277   result    = [[NSMutableArray      alloc] initWithCapacity:fieldCount];
278   usedNames = [[NSMutableDictionary alloc] initWithCapacity:fieldCount];
279
280   for (cnt = 0; cnt < fieldCount; cnt++) {
281     EOAttribute *attribute  = nil;
282     NSString    *columnName = nil;
283     NSString    *attrName   = nil;
284     
285     columnName = [NSString stringWithUTF8String:mfields[cnt].name];
286     attrName   = _beautifyNames
287       ? [columnName _mySQL4ModelMakeInstanceVarName] 
288       : columnName;
289     
290     if ([[usedNames objectForKey:attrName] boolValue]) {
291       int      cnt2 = 0;
292       char     buf[64];
293       NSString *newAttrName = nil;
294
295       for (cnt2 = 2; cnt2 < 100; cnt2++) {
296         NSString *s;
297         sprintf(buf, "%i", cnt2);
298         
299         // TODO: unicode
300         s = [[NSString alloc] initWithCString:buf];
301         newAttrName = [attrName stringByAppendingString:s];
302         [s release];
303         
304         if (![[usedNames objectForKey:newAttrName] boolValue]) {
305           attrName = newAttrName;
306           break;
307         }
308       }
309     }
310     [usedNames setObject:yesObj forKey:attrName];
311
312     attribute = [[EOAttribute alloc] init];
313     [attribute setName:attrName];
314     [attribute setColumnName:columnName];
315     
316     [attribute setAllowsNull:
317                  (mfields[cnt].flags & NOT_NULL_FLAG) ? NO : YES];
318     
319     /*
320       We also know whether a field:
321         is primary
322         is unique
323         is auto-increment
324         is zero-fill
325         is unsigned
326     */
327     if (mfields[cnt].flags & UNSIGNED_FLAG) {
328       NSLog(@"ERROR: MySQL4 field is marked unsigned (unsupported): %@",
329             attribute);
330     }
331     
332     switch (mfields[cnt].type) {
333     case FIELD_TYPE_STRING:
334       [attribute setExternalType:@"CHAR"];
335       [attribute setValueClassName:@"NSString"];
336       // TODO: length etc
337       break;
338     case FIELD_TYPE_VAR_STRING:
339       [attribute setExternalType:@"VARCHAR"];
340       [attribute setValueClassName:@"NSString"];
341       // TODO: length etc
342       break;
343       
344     case FIELD_TYPE_TINY:
345       [attribute setExternalType:@"TINY"];
346       [attribute setValueClassName:@"NSNumber"];
347       [attribute setValueType:@"c"];
348       break;
349     case FIELD_TYPE_SHORT:
350       [attribute setExternalType:@"SHORT"];
351       [attribute setValueClassName:@"NSNumber"];
352       [attribute setValueType:@"s"];
353       break;
354     case FIELD_TYPE_LONG:
355       [attribute setExternalType:@"LONG"];
356       [attribute setValueClassName:@"NSNumber"];
357       [attribute setValueType:@"l"];
358       break;
359     case FIELD_TYPE_INT24:
360       [attribute setExternalType:@"INT"];
361       [attribute setValueClassName:@"NSNumber"];
362       [attribute setValueType:@"i"]; // bumped
363       break;
364     case FIELD_TYPE_LONGLONG:
365       [attribute setExternalType:@"LONGLONG"];
366       [attribute setValueClassName:@"NSNumber"];
367       [attribute setValueType:@"q"];
368       break;
369     case FIELD_TYPE_DECIMAL:
370       [attribute setExternalType:@"DECIMAL"];
371       [attribute setValueClassName:@"NSNumber"];
372       [attribute setValueType:@"f"]; // TODO: need NSDecimalNumber here ...
373       break;
374     case FIELD_TYPE_FLOAT:
375       [attribute setExternalType:@"FLOAT"];
376       [attribute setValueClassName:@"NSNumber"];
377       [attribute setValueType:@"f"];
378       break;
379     case FIELD_TYPE_DOUBLE:
380       [attribute setExternalType:@"DOUBLE"];
381       [attribute setValueClassName:@"NSNumber"];
382       [attribute setValueType:@"d"];
383       break;
384
385     case FIELD_TYPE_TIMESTAMP:
386       [attribute setExternalType:@"TIMESTAMP"];
387       [attribute setValueClassName:@"NSCalendarDate"];
388       break;
389     case FIELD_TYPE_DATE:
390       [attribute setExternalType:@"DATE"];
391       [attribute setValueClassName:@"NSCalendarDate"];
392       break;
393     case FIELD_TYPE_DATETIME:
394       [attribute setExternalType:@"DATETIME"];
395       [attribute setValueClassName:@"NSCalendarDate"];
396       break;
397       
398     case FIELD_TYPE_BLOB:
399     case FIELD_TYPE_TINY_BLOB:
400     case FIELD_TYPE_MEDIUM_BLOB:
401     case FIELD_TYPE_LONG_BLOB:
402       // TODO: length etc
403       if (mfields[cnt].flags & BINARY_FLAG) {
404         [attribute setExternalType:@"BLOB"];
405         [attribute setValueClassName:@"NSData"];
406       }
407       else {
408         [attribute setExternalType:@"TEXT"];
409         [attribute setValueClassName:@"NSString"];
410       }
411       break;
412       
413     case FIELD_TYPE_NULL: // TODO: whats that?
414     case FIELD_TYPE_TIME:
415     case FIELD_TYPE_YEAR:
416     case FIELD_TYPE_SET:
417     case FIELD_TYPE_ENUM:
418     default:
419         NSLog(@"ERROR(%s): unexpected MySQL4 type at column %i: %@", 
420               __PRETTY_FUNCTION__, cnt, attribute);
421         break;
422     }
423     
424     [result addObject:attribute];
425     [attribute release];
426   }
427
428   [usedNames release];
429   usedNames = nil;
430   
431   return [result autorelease];
432 }
433 - (NSArray *)describeResults {
434   return [self describeResults:NO];
435 }
436
437 - (NSMutableDictionary *)primaryFetchAttributes:(NSArray *)_attributes
438   withZone:(NSZone *)_zone
439 {
440   /*
441     Note: we expect that the attributes match the generated SQL. This is
442           because auto-generated SQL can contain SQL table prefixes (like
443           alias.column-name which cannot be detected using the attributes
444           schema)
445   */
446   // TODO: add a primaryFetchAttributesX method?
447   MYSQL_ROW rawRow;
448   NSMutableDictionary *row = nil;
449   unsigned attrCount = [_attributes count];
450   unsigned cnt;
451   unsigned long *lengths;
452   
453   if (self->results == NULL) {
454     NSLog(@"ERROR(%s): no fetch in progress?", __PRETTY_FUNCTION__);
455     [self cancelFetch];
456     return nil;
457   }
458
459   /* raw fetch */
460   
461   if ((rawRow = mysql_fetch_row(self->results)) == NULL) {
462     // TODO: might need to close channel on connect exceptions
463     unsigned int merrno;
464     
465     if ((merrno = mysql_errno(self->_connection)) != 0) {
466       const char *error;
467       
468       error = mysql_error(self->_connection);
469       [MySQL4Exception raise:@"FetchFailed" 
470                        format:@"%@",[NSString stringWithUTF8String:error]];
471       return nil;
472     }
473     
474     /* regular end of result set */
475     [self cancelFetch];
476     return nil;
477   }
478
479   /* ensure field info */
480   
481   if ([self _fetchFields] == NULL) {
482     [self cancelFetch];
483     [MySQL4Exception raise:@"FetchFailed" 
484                      format:@"could not fetch field info!"];
485     return nil;
486   }
487   
488   if ((lengths = mysql_fetch_lengths(self->results)) == NULL) {
489     [self cancelFetch];
490     [MySQL4Exception raise:@"FetchFailed" 
491                      format:@"could not fetch field lengths!"];
492     return nil;
493   }
494   
495   /* build row */
496   
497   row = [NSMutableDictionary dictionaryWithCapacity:attrCount];
498   
499   for (cnt = 0; cnt < attrCount; cnt++) {
500     EOAttribute *attribute;
501     NSString    *attrName;
502     id          value      = nil;
503     MYSQL_FIELD mfield;
504     
505     attribute = [_attributes objectAtIndex:cnt];
506     attrName  = [attribute name];
507     mfield    = ((MYSQL_FIELD *)self->fields)[cnt];
508     
509     if (rawRow[cnt] == NULL) {
510       value = [null retain];
511     }
512     else {
513       Class valueClass;
514       
515       valueClass = NSClassFromString([attribute valueClassName]);
516       if (valueClass == Nil) {
517         NSLog(@"ERROR(%s): %@: got no value class for column:\n"
518               @"  attribute=%@\n  type=%@",
519               __PRETTY_FUNCTION__, self,
520               attrName, [attribute externalType]);
521         value = null;
522         continue;
523       }
524
525       value = [[valueClass alloc] initWithMySQL4Type:mfield.type
526                                   value:rawRow[cnt] length:lengths[cnt]];
527       
528       if (value == nil) {
529         NSLog(@"ERROR(%s): %@: got no value for column:\n"
530               @"  attribute=%@\n  valueClass=%@\n  type=%@",
531               __PRETTY_FUNCTION__, self,
532               attrName, NSStringFromClass(valueClass), 
533               [attribute externalType]);
534         continue;
535       }
536     }
537     if (value != nil) {
538       [row setObject:value forKey:attrName];
539       [value release];
540     }
541   }
542   
543   return row;
544 }
545
546 /* sending SQL to server */
547
548 - (NSException *)evaluateExpressionX:(NSString *)_expression {
549   NSMutableString *sql;
550   BOOL       result;
551   const char *s;
552   int  rc;
553
554   *(&result) = YES;
555   
556   if (_expression == nil) {
557     return [NSException exceptionWithName:@"InvalidArgumentException"
558                         reason:
559                           @"parameter for evaluateExpression: must not be null"
560                         userInfo:nil];
561   }
562   
563   sql = [[_expression mutableCopy] autorelease];
564   [sql appendString:@";"];
565
566   /* ask delegate */
567   
568   if (delegateRespondsTo.willEvaluateExpression) {
569     EODelegateResponse response;
570     
571     response = [delegate adaptorChannel:self willEvaluateExpression:sql];
572     
573     if (response == EODelegateRejects) {
574       return [NSException exceptionWithName:@"EODelegateRejects"
575                           reason:@"delegate rejected insert"
576                           userInfo:nil];
577     }
578     if (response == EODelegateOverrides)
579       return nil;
580   }
581
582   /* check some preconditions */
583   
584   if (![self isOpen]) {
585     return [MySQL4Exception exceptionWithName:@"ChannelNotOpenException"
586                             reason:@"MySQL4 connection is not open"
587                             userInfo:nil];
588   }
589   if (self->results != NULL) {
590     return [MySQL4Exception exceptionWithName:@"CommandInProgressException"
591                             reason:@"an evaluation is in progress"
592                             userInfo:nil];
593     return NO;
594   }
595   
596   if ([self isFetchInProgress]) {
597     NSLog(@"WARNING: a fetch is still in progress: %@", self);
598     [self cancelFetch];
599   }
600   
601   if (isDebuggingEnabled)
602     NSLog(@"%@ SQL: %@", self, sql);
603   
604   /* reset environment */
605   
606   self->isFetchInProgress = NO;
607   
608   /* start query */
609   
610   s  = [sql UTF8String];
611   if ((rc = mysql_real_query(self->_connection, s, strlen(s))) != 0) {
612     // TODO: might need to close channel on connect exceptions
613     const char *error;
614     
615     error = mysql_error(self->_connection);
616     if (isDebuggingEnabled)
617       NSLog(@"%@   ERROR: %s", self, error);
618     
619     return [MySQL4Exception exceptionWithName:@"ExecutionFailed" 
620                             reason:[NSString stringWithUTF8String:error]
621                             userInfo:nil];
622   }
623   
624   /* fetch */
625   
626   if ((self->results = mysql_use_result(self->_connection)) != NULL) {
627     if (isDebuggingEnabled)
628       NSLog(@"%@   query has results, entering fetch-mode.", self);
629     self->isFetchInProgress = YES;
630   }
631   else {
632     /* error _OR_ statement without result-set */
633     unsigned int merrno;
634
635     if ((merrno = mysql_errno(self->_connection)) != 0) {
636       const char *error;
637       
638       if (isDebuggingEnabled)
639         NSLog(@"%@   cannot use result: '%s'", self, error);
640       
641       error = mysql_error(self->_connection);
642       return [MySQL4Exception exceptionWithName:@"FetchFailed" 
643                               reason:[NSString stringWithUTF8String:error]
644                               userInfo:nil];
645     }
646     
647     if (isDebuggingEnabled)
648       NSLog(@"%@   query has no results.", self);
649   }
650   
651   if (delegateRespondsTo.didEvaluateExpression)
652     [delegate adaptorChannel:self didEvaluateExpression:sql];
653   
654   return nil /* everything is OK */;
655 }
656 - (BOOL)evaluateExpression:(NSString *)_sql {
657   NSException *e;
658   NSString *n;
659   
660   if ((e = [self evaluateExpressionX:_sql]) == nil)
661     return YES;
662   
663   /* for compatibility with non-X methods, translate some errors to a bool */
664   n = [e name];
665   if ([n isEqualToString:@"EOEvaluationError"])
666     return NO;
667   if ([n isEqualToString:@"EODelegateRejects"])
668     return NO;
669
670   NSLog(@"ERROR eval '%@': %@", _sql, e);
671   
672   [e raise];
673   return NO;
674 }
675
676 /* description */
677
678 - (NSString *)description {
679   NSMutableString *ms;
680
681   ms = [NSMutableString stringWithCapacity:64];
682   [ms appendFormat:@"<%@[0x%08X] connection=0x%08X",
683                      NSStringFromClass([self class]),
684                      self,
685                      (unsigned)self->_connection];
686   [ms appendString:@">"];
687   return ms;
688 }
689
690 /* PrimaryKeyGeneration */
691
692 - (NSDictionary *)primaryKeyForNewRowWithEntity:(EOEntity *)_entity {
693   NSException   *error;
694   NSArray       *pkeys;
695   MySQL4Adaptor *adaptor;
696   NSString      *seqName, *seq;
697   NSArray       *seqs;
698   NSDictionary  *pkey;
699   unsigned      i, count;
700   id key;
701   
702   pkeys   = [_entity primaryKeyAttributeNames];
703   adaptor = (id)[[self adaptorContext] adaptor];
704   seqName = [adaptor primaryKeySequenceName];
705   pkey    = nil;
706   seq     = nil;
707   
708   if ([seqName length] > 0) {
709     // TODO: if we do this, we also need to make the 'id' configurable ...
710     seq = [@"UPDATE " stringByAppendingString:seqName];
711     seq = [seq stringByAppendingString:@" SET id=LAST_INSERT_ID(id+1)"];
712     seqs = [NSArray arrayWithObjects:
713                       seq, @"SELECT_LAST_INSERT_ID()", nil];
714   }
715   else
716     seqs = [[adaptor newKeyExpression] componentsSeparatedByString:@";"];
717
718   if ((count = [seqs count]) == 0) {
719     NSLog(@"ERROR(%@): got no primary key expressions %@: %@", 
720           self, seqName, _entity);
721     return nil;
722   }
723
724   for (i = 0; i < count - 1; i++) {
725     if ((error = [self evaluateExpressionX:[seqs objectAtIndex:i]]) != nil) {
726       NSLog(@"ERROR(%@): could not prepare next pkey value %@: %@", 
727             self, [seqs objectAtIndex:i], error);
728       return nil;
729     }
730   }
731   
732   seq = [seqs lastObject];
733   if ((error = [self evaluateExpressionX:seq]) != nil) {
734     NSLog(@"ERROR(%@): could not select next pkey value from sequence %@: %@", 
735           self, seqName, error);
736     return nil;
737   }
738   
739   if (![self isFetchInProgress]) {
740     NSLog(@"ERROR(%@): primary key expression returned no result: '%@'",
741           self, seq);
742     return nil;
743   }
744   
745   // TODO: this is kinda slow
746   key  = [self describeResults];
747   pkey = [self fetchAttributes:key withZone:NULL];
748   
749   [self cancelFetch];
750   
751   if (pkey != nil) {
752     pkey = [[pkey allValues] lastObject];
753     pkey = [NSDictionary dictionaryWithObject:pkey
754                          forKey:[pkeys objectAtIndex:0]];
755   }
756   
757   return pkey;
758 }
759
760 @end /* MySQL4Channel */
761
762 void __link_MySQL4Channel() {
763   // used to force linking of object file
764   __link_MySQL4Channel();
765 }