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