4 Copyright (C) 2003-2005 SKYRIX Software AG
6 Author: Helge Hess (helge.hess@skyrix.com)
8 This file is part of the MySQL4 Adaptor Library
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.
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.
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.
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"
36 #include <mysql/mysql.h>
39 # define MIN(x, y) ((x > y) ? y : x)
42 #define MAX_CHAR_BUF 16384
44 @implementation MySQL4Channel
46 static EONull *null = nil;
49 if (null == NULL) null = [[EONull null] retain];
52 - (id)initWithAdaptorContext:(EOAdaptorContext*)_adaptorContext {
53 if ((self = [super initWithAdaptorContext:_adaptorContext])) {
54 [self setDebugEnabled:[[NSUserDefaults standardUserDefaults]
55 boolForKey:@"MySQL4DebugEnabled"]];
57 self->_attributesForTableName =
58 [[NSMutableDictionary alloc] initWithCapacity:16];
59 self->_primaryKeysNamesForTableName =
60 [[NSMutableDictionary alloc] initWithCapacity:16];
65 - (void)_adaptorWillFinalize:(id)_adaptor {
71 [self->_attributesForTableName release];
72 [self->_primaryKeysNamesForTableName release];
76 /* NSCopying methods */
78 - (id)copyWithZone:(NSZone *)zone {
84 - (void)setDebugEnabled:(BOOL)_flag {
85 self->isDebuggingEnabled = _flag;
87 - (BOOL)isDebugEnabled {
88 return self->isDebuggingEnabled;
91 - (void)receivedMessage:(NSString *)_message {
92 NSLog(@"%@: message %@.", _message);
97 static int openConnectionCount = 0;
100 return (self->_connection != NULL) ? YES : NO;
103 - (int)maxOpenConnectionCount {
104 static int MaxOpenConnectionCount = -1;
106 if (MaxOpenConnectionCount != -1)
107 return MaxOpenConnectionCount;
109 MaxOpenConnectionCount =
110 [[NSUserDefaults standardUserDefaults]
111 integerForKey:@"MySQL4MaxOpenConnectionCount"];
112 if (MaxOpenConnectionCount == 0)
113 MaxOpenConnectionCount = 150;
114 return MaxOpenConnectionCount;
117 - (BOOL)openChannel {
119 MySQL4Adaptor *adaptor;
120 NSString *host, *socket;
123 if (self->_connection != NULL) {
124 NSLog(@"%s: Connection already open !!!", __PRETTY_FUNCTION__);
128 adaptor = (MySQL4Adaptor *)[adaptorContext adaptor];
130 if (![super openChannel])
133 if (openConnectionCount > [self maxOpenConnectionCount]) {
134 [MySQL4CouldNotOpenChannelException
135 raise:@"NoMoreConnections"
136 format:@"cannot open a additional connection !"];
140 cDBName = [[adaptor databaseName] UTF8String];
142 if ((self->_connection = mysql_init(NULL)) == NULL) {
143 NSLog(@"ERROR(%s): could not allocate MySQL4 connection!");
147 // TODO: could change options using mysql_options()
149 host = [adaptor serverName];
150 if ([host hasPrefix:@"/"]) { /* treat hostname as Unix socket path */
157 rc = mysql_real_connect(self->_connection,
159 [[adaptor loginName] UTF8String],
160 [[adaptor loginPassword] UTF8String],
162 [[adaptor port] intValue],
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;
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));
177 mysql_close(self->_connection);
178 self->_connection = NULL;
183 if (isDebuggingEnabled)
184 NSLog(@"MySQL4 connection established 0x%08X", self->_connection);
187 NSLog(@"---------- %s: %@ opens channel count[%d]", __PRETTY_FUNCTION__,
188 self, openConnectionCount);
190 openConnectionCount++;
192 #if LIB_FOUNDATION_BOEHM_GC
193 [GarbageCollector registerForFinalizationObserver:self
194 selector:@selector(_adaptorWillFinalize:)
195 object:[[self adaptorContext] adaptor]];
198 if (isDebuggingEnabled) {
199 NSLog(@"MySQL4 channel 0x%08X opened (connection=0x%08X,%s)",
200 (unsigned)self, self->_connection, cDBName);
205 - (void)primaryCloseChannel {
206 if ([self isFetchInProgress])
209 if (self->_connection != NULL) {
210 mysql_close(self->_connection);
212 NSLog(@"---------- %s: %@ close channel count[%d]", __PRETTY_FUNCTION__,
213 self, openConnectionCount);
215 openConnectionCount--;
217 if (isDebuggingEnabled) {
219 "MySQL4 connection dropped 0x%08X (channel=0x%08X)\n",
220 (unsigned)self->_connection, (unsigned)self);
222 self->_connection = NULL;
226 - (void)closeChannel {
227 [super closeChannel];
228 [self primaryCloseChannel];
233 - (void)cancelFetch {
234 self->fields = NULL; /* apparently we do not need to free those */
236 if (self->results != NULL) {
237 mysql_free_result(self->results);
238 self->results = NULL;
243 - (MYSQL_FIELD *)_fetchFields {
244 if (self->results == NULL)
247 if (self->fields != NULL)
250 self->fields = mysql_fetch_fields(self->results);
251 self->fieldCount = mysql_num_fields(self->results);
255 - (NSArray *)describeResults:(BOOL)_beautifyNames {
256 // TODO: make exception-less method
257 MYSQL_FIELD *mfields;
259 NSMutableArray *result = nil;
260 NSMutableDictionary *usedNames = nil;
263 yesObj = [NSNumber numberWithBool:YES];
265 if (![self isFetchInProgress]) {
266 [MySQL4Exception raise:@"NoFetchInProgress"
267 format:@"No fetch in progress (channel=%@)", self];
271 if ((mfields = [self _fetchFields]) == NULL) {
272 [MySQL4Exception raise:@"NoFieldInfo"
273 format:@"Failed to fetch field info (channel=%@)", self];
277 result = [[NSMutableArray alloc] initWithCapacity:fieldCount];
278 usedNames = [[NSMutableDictionary alloc] initWithCapacity:fieldCount];
280 for (cnt = 0; cnt < fieldCount; cnt++) {
281 EOAttribute *attribute = nil;
282 NSString *columnName = nil;
283 NSString *attrName = nil;
285 columnName = [NSString stringWithUTF8String:mfields[cnt].name];
286 attrName = _beautifyNames
287 ? [columnName _mySQL4ModelMakeInstanceVarName]
290 if ([[usedNames objectForKey:attrName] boolValue]) {
293 NSString *newAttrName = nil;
295 for (cnt2 = 2; cnt2 < 100; cnt2++) {
297 sprintf(buf, "%i", cnt2);
300 s = [[NSString alloc] initWithCString:buf];
301 newAttrName = [attrName stringByAppendingString:s];
304 if (![[usedNames objectForKey:newAttrName] boolValue]) {
305 attrName = newAttrName;
310 [usedNames setObject:yesObj forKey:attrName];
312 attribute = [[EOAttribute alloc] init];
313 [attribute setName:attrName];
314 [attribute setColumnName:columnName];
316 [attribute setAllowsNull:
317 (mfields[cnt].flags & NOT_NULL_FLAG) ? NO : YES];
320 We also know whether a field:
327 if (mfields[cnt].flags & UNSIGNED_FLAG) {
328 NSLog(@"ERROR: MySQL4 field is marked unsigned (unsupported): %@",
332 switch (mfields[cnt].type) {
333 case FIELD_TYPE_STRING:
334 [attribute setExternalType:@"CHAR"];
335 [attribute setValueClassName:@"NSString"];
338 case FIELD_TYPE_VAR_STRING:
339 [attribute setExternalType:@"VARCHAR"];
340 [attribute setValueClassName:@"NSString"];
344 case FIELD_TYPE_TINY:
345 [attribute setExternalType:@"TINY"];
346 [attribute setValueClassName:@"NSNumber"];
347 [attribute setValueType:@"c"];
349 case FIELD_TYPE_SHORT:
350 [attribute setExternalType:@"SHORT"];
351 [attribute setValueClassName:@"NSNumber"];
352 [attribute setValueType:@"s"];
354 case FIELD_TYPE_LONG:
355 [attribute setExternalType:@"LONG"];
356 [attribute setValueClassName:@"NSNumber"];
357 [attribute setValueType:@"l"];
359 case FIELD_TYPE_INT24:
360 [attribute setExternalType:@"INT"];
361 [attribute setValueClassName:@"NSNumber"];
362 [attribute setValueType:@"i"]; // bumped
364 case FIELD_TYPE_LONGLONG:
365 [attribute setExternalType:@"LONGLONG"];
366 [attribute setValueClassName:@"NSNumber"];
367 [attribute setValueType:@"q"];
369 case FIELD_TYPE_DECIMAL:
370 [attribute setExternalType:@"DECIMAL"];
371 [attribute setValueClassName:@"NSNumber"];
372 [attribute setValueType:@"f"]; // TODO: need NSDecimalNumber here ...
374 case FIELD_TYPE_FLOAT:
375 [attribute setExternalType:@"FLOAT"];
376 [attribute setValueClassName:@"NSNumber"];
377 [attribute setValueType:@"f"];
379 case FIELD_TYPE_DOUBLE:
380 [attribute setExternalType:@"DOUBLE"];
381 [attribute setValueClassName:@"NSNumber"];
382 [attribute setValueType:@"d"];
385 case FIELD_TYPE_TIMESTAMP:
386 [attribute setExternalType:@"TIMESTAMP"];
387 [attribute setValueClassName:@"NSCalendarDate"];
389 case FIELD_TYPE_DATE:
390 [attribute setExternalType:@"DATE"];
391 [attribute setValueClassName:@"NSCalendarDate"];
393 case FIELD_TYPE_DATETIME:
394 [attribute setExternalType:@"DATETIME"];
395 [attribute setValueClassName:@"NSCalendarDate"];
398 case FIELD_TYPE_BLOB:
399 case FIELD_TYPE_TINY_BLOB:
400 case FIELD_TYPE_MEDIUM_BLOB:
401 case FIELD_TYPE_LONG_BLOB:
403 if (mfields[cnt].flags & BINARY_FLAG) {
404 [attribute setExternalType:@"BLOB"];
405 [attribute setValueClassName:@"NSData"];
408 [attribute setExternalType:@"TEXT"];
409 [attribute setValueClassName:@"NSString"];
413 case FIELD_TYPE_NULL: // TODO: whats that?
414 case FIELD_TYPE_TIME:
415 case FIELD_TYPE_YEAR:
417 case FIELD_TYPE_ENUM:
419 NSLog(@"ERROR(%s): unexpected MySQL4 type at column %i: %@",
420 __PRETTY_FUNCTION__, cnt, attribute);
424 [result addObject:attribute];
431 return [result autorelease];
433 - (NSArray *)describeResults {
434 return [self describeResults:NO];
437 - (NSMutableDictionary *)primaryFetchAttributes:(NSArray *)_attributes
438 withZone:(NSZone *)_zone
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
446 // TODO: add a primaryFetchAttributesX method?
448 NSMutableDictionary *row = nil;
449 unsigned attrCount = [_attributes count];
451 unsigned long *lengths;
453 if (self->results == NULL) {
454 NSLog(@"ERROR(%s): no fetch in progress?", __PRETTY_FUNCTION__);
461 if ((rawRow = mysql_fetch_row(self->results)) == NULL) {
462 // TODO: might need to close channel on connect exceptions
465 if ((merrno = mysql_errno(self->_connection)) != 0) {
468 error = mysql_error(self->_connection);
469 [MySQL4Exception raise:@"FetchFailed"
470 format:@"%@",[NSString stringWithUTF8String:error]];
474 /* regular end of result set */
479 /* ensure field info */
481 if ([self _fetchFields] == NULL) {
483 [MySQL4Exception raise:@"FetchFailed"
484 format:@"could not fetch field info!"];
488 if ((lengths = mysql_fetch_lengths(self->results)) == NULL) {
490 [MySQL4Exception raise:@"FetchFailed"
491 format:@"could not fetch field lengths!"];
497 row = [NSMutableDictionary dictionaryWithCapacity:attrCount];
499 for (cnt = 0; cnt < attrCount; cnt++) {
500 EOAttribute *attribute;
505 attribute = [_attributes objectAtIndex:cnt];
506 attrName = [attribute name];
507 mfield = ((MYSQL_FIELD *)self->fields)[cnt];
509 if (rawRow[cnt] == NULL) {
510 value = [null retain];
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]);
525 value = [[valueClass alloc] initWithMySQL4Type:mfield.type
526 value:rawRow[cnt] length:lengths[cnt]];
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]);
538 [row setObject:value forKey:attrName];
546 /* sending SQL to server */
548 - (NSException *)evaluateExpressionX:(NSString *)_expression {
549 NSMutableString *sql;
556 if (_expression == nil) {
557 return [NSException exceptionWithName:@"InvalidArgumentException"
559 @"parameter for evaluateExpression: must not be null"
563 sql = [[_expression mutableCopy] autorelease];
564 [sql appendString:@";"];
568 if (delegateRespondsTo.willEvaluateExpression) {
569 EODelegateResponse response;
571 response = [delegate adaptorChannel:self willEvaluateExpression:sql];
573 if (response == EODelegateRejects) {
574 return [NSException exceptionWithName:@"EODelegateRejects"
575 reason:@"delegate rejected insert"
578 if (response == EODelegateOverrides)
582 /* check some preconditions */
584 if (![self isOpen]) {
585 return [MySQL4Exception exceptionWithName:@"ChannelNotOpenException"
586 reason:@"MySQL4 connection is not open"
589 if (self->results != NULL) {
590 return [MySQL4Exception exceptionWithName:@"CommandInProgressException"
591 reason:@"an evaluation is in progress"
596 if ([self isFetchInProgress]) {
597 NSLog(@"WARNING: a fetch is still in progress: %@", self);
601 if (isDebuggingEnabled)
602 NSLog(@"%@ SQL: %@", self, sql);
604 /* reset environment */
606 self->isFetchInProgress = NO;
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
615 error = mysql_error(self->_connection);
616 if (isDebuggingEnabled)
617 NSLog(@"%@ ERROR: %s", self, error);
619 return [MySQL4Exception exceptionWithName:@"ExecutionFailed"
620 reason:[NSString stringWithUTF8String:error]
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;
632 /* error _OR_ statement without result-set */
635 if ((merrno = mysql_errno(self->_connection)) != 0) {
638 if (isDebuggingEnabled)
639 NSLog(@"%@ cannot use result: '%s'", self, error);
641 error = mysql_error(self->_connection);
642 return [MySQL4Exception exceptionWithName:@"FetchFailed"
643 reason:[NSString stringWithUTF8String:error]
647 if (isDebuggingEnabled)
648 NSLog(@"%@ query has no results.", self);
651 if (delegateRespondsTo.didEvaluateExpression)
652 [delegate adaptorChannel:self didEvaluateExpression:sql];
654 return nil /* everything is OK */;
656 - (BOOL)evaluateExpression:(NSString *)_sql {
660 if ((e = [self evaluateExpressionX:_sql]) == nil)
663 /* for compatibility with non-X methods, translate some errors to a bool */
665 if ([n isEqualToString:@"EOEvaluationError"])
667 if ([n isEqualToString:@"EODelegateRejects"])
670 NSLog(@"ERROR eval '%@': %@", _sql, e);
678 - (NSString *)description {
681 ms = [NSMutableString stringWithCapacity:64];
682 [ms appendFormat:@"<%@[0x%08X] connection=0x%08X",
683 NSStringFromClass([self class]),
685 (unsigned)self->_connection];
686 [ms appendString:@">"];
690 /* PrimaryKeyGeneration */
692 - (NSDictionary *)primaryKeyForNewRowWithEntity:(EOEntity *)_entity {
695 MySQL4Adaptor *adaptor;
696 NSString *seqName, *seq;
702 pkeys = [_entity primaryKeyAttributeNames];
703 adaptor = (id)[[self adaptorContext] adaptor];
704 seqName = [adaptor primaryKeySequenceName];
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];
716 seqs = [[adaptor newKeyExpression] componentsSeparatedByString:@";"];
718 if ((count = [seqs count]) == 0) {
719 NSLog(@"ERROR(%@): got no primary key expressions %@: %@",
720 self, seqName, _entity);
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);
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);
739 if (![self isFetchInProgress]) {
740 NSLog(@"ERROR(%@): primary key expression returned no result: '%@'",
745 // TODO: this is kinda slow
746 key = [self describeResults];
747 pkey = [self fetchAttributes:key withZone:NULL];
752 pkey = [[pkey allValues] lastObject];
753 pkey = [NSDictionary dictionaryWithObject:pkey
754 forKey:[pkeys objectAtIndex:0]];
760 @end /* MySQL4Channel */
762 void __link_MySQL4Channel() {
763 // used to force linking of object file
764 __link_MySQL4Channel();