]> err.no Git - sope/blob - sope-gdl1/SQLite3/SQLiteChannel.m
Added SQLite3 adaptor, bumped version informations
[sope] / sope-gdl1 / SQLite3 / SQLiteChannel.m
1 /* 
2    SQLiteChannel.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 SQLite 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 "SQLiteChannel.h"
30 #include "SQLiteAdaptor.h"
31 #include "SQLiteException.h"
32 #include "NSString+SQLite.h"
33 #include "SQLiteValues.h"
34 #include "EOAttribute+SQLite.h"
35 #include "common.h"
36
37 #ifndef MIN
38 #  define MIN(x, y) ((x > y) ? y : x)
39 #endif
40
41 #define MAX_CHAR_BUF 16384
42
43 @implementation SQLiteChannel
44
45 static EONull *null = nil;
46
47 + (void)initialize {
48   if (null == NULL) null = [[EONull null] retain];
49 }
50
51 - (id)initWithAdaptorContext:(EOAdaptorContext*)_adaptorContext {
52   if ((self = [super initWithAdaptorContext:_adaptorContext])) {
53     [self setDebugEnabled:[[NSUserDefaults standardUserDefaults]
54                                            boolForKey:@"SQLiteDebugEnabled"]];
55     
56     self->_attributesForTableName = 
57       [[NSMutableDictionary alloc] initWithCapacity:16];
58     self->_primaryKeysNamesForTableName =
59       [[NSMutableDictionary alloc] initWithCapacity:16];
60   }
61   return self;
62 }
63
64 - (void)_adaptorWillFinalize:(id)_adaptor {
65 }
66
67 - (void)dealloc {
68   if ([self isOpen])
69     [self closeChannel];
70   [self->_attributesForTableName       release];
71   [self->_primaryKeysNamesForTableName release];
72   [super dealloc];
73 }
74
75 /* NSCopying methods */
76
77 - (id)copyWithZone:(NSZone *)zone {
78   return [self retain];
79 }
80
81 // debugging
82
83 - (void)setDebugEnabled:(BOOL)_flag {
84   self->isDebuggingEnabled = _flag;
85 }
86 - (BOOL)isDebugEnabled {
87   return self->isDebuggingEnabled;
88 }
89
90 - (void)receivedMessage:(NSString *)_message {
91   NSLog(@"%@: message %@.", _message);
92 }
93
94 /* open/close */
95
96 static int openConnectionCount = 0;
97
98 - (BOOL)isOpen {
99   return (self->_connection != NULL) ? YES : NO;
100 }
101
102 - (int)maxOpenConnectionCount {
103   static int MaxOpenConnectionCount = -1;
104     
105   if (MaxOpenConnectionCount != -1)
106     return MaxOpenConnectionCount;
107
108   MaxOpenConnectionCount =
109     [[NSUserDefaults standardUserDefaults]
110                      integerForKey:@"SQLiteMaxOpenConnectionCount"];
111   if (MaxOpenConnectionCount == 0)
112     MaxOpenConnectionCount = 15;
113   return MaxOpenConnectionCount;
114 }
115
116 - (BOOL)openChannel {
117   const unsigned char *cDBName;
118   SQLiteAdaptor *adaptor;
119   int rc;
120   
121   if (self->_connection) {
122     NSLog(@"%s: Connection already open !!!", __PRETTY_FUNCTION__);
123     return NO;
124   }
125   
126   adaptor = (SQLiteAdaptor *)[adaptorContext adaptor];
127   
128   if (![super openChannel])
129     return NO;
130
131   if (openConnectionCount > [self maxOpenConnectionCount]) {
132     [SQLiteCouldNotOpenChannelException 
133         raise:@"NoMoreConnections"
134         format:@"cannot open a additional connection !"];
135     return NO;
136   }
137
138   cDBName = [[adaptor databaseName] UTF8String];
139   
140   rc = sqlite3_open(cDBName, (void *)&(self->_connection));
141   if (rc != SQLITE_OK) {
142     // could not login ..
143     // Note: connection *is* set! (might be required to deallocate)
144     NSLog(@"WARNING: could not open SQLite connection to database '%@': %s",
145           [adaptor databaseName], sqlite3_errmsg(self->_connection));
146     sqlite3_close(self->_connection);
147     return NO;
148   }
149   
150   if (isDebuggingEnabled)
151     NSLog(@"SQLite connection established 0x%08X", self->_connection);
152
153 #if 0
154   NSLog(@"---------- %s: %@ opens channel count[%d]", __PRETTY_FUNCTION__,
155         self, openConnectionCount);
156 #endif
157   openConnectionCount++;
158   
159 #if LIB_FOUNDATION_BOEHM_GC
160   [GarbageCollector registerForFinalizationObserver:self
161                     selector:@selector(_adaptorWillFinalize:)
162                     object:[[self adaptorContext] adaptor]];
163 #endif
164
165   if (isDebuggingEnabled) {
166     NSLog(@"SQLite channel 0x%08X opened (connection=0x%08X,%s)",
167           (unsigned)self, self->_connection, cDBName);
168   }
169   return YES;
170 }
171
172 - (void)primaryCloseChannel {
173   if (self->statement != NULL) {
174     sqlite3_finalize(self->statement);
175     self->statement = NULL;
176   }
177   
178   if (self->_connection != NULL) {
179     sqlite3_close(self->_connection);
180 #if 0
181     NSLog(@"---------- %s: %@ close channel count[%d]", __PRETTY_FUNCTION__,
182           self, openConnectionCount);
183 #endif
184     openConnectionCount--;
185     
186     if (isDebuggingEnabled) {
187       fprintf(stderr, 
188               "SQLite connection dropped 0x%08X (channel=0x%08X)\n",
189               (unsigned)self->_connection, (unsigned)self);
190     }
191     self->_connection = NULL;
192   }
193 }
194
195 - (void)closeChannel {
196   [super closeChannel];
197   [self primaryCloseChannel];
198 }
199
200 /* fetching rows */
201
202 - (NSException *)_makeSQLiteStep {
203   NSString *r;
204   const char *em;
205   int rc;
206   
207   rc = sqlite3_step(self->statement);
208 #if 0
209   NSLog(@"STEP: %i (row=%i, done=%i, mis=%i)", rc,
210         SQLITE_ROW, SQLITE_DONE, SQLITE_MISUSE);
211 #endif
212   
213   if (rc == SQLITE_ROW) {
214     self->hasPendingRow = YES;
215     self->isDone        = NO;
216     return nil /* no error */;
217   }
218   if (rc == SQLITE_DONE) {
219     self->hasPendingRow = NO;
220     self->isDone        = YES;
221     return nil /* no error */;
222   }
223
224   if (rc == SQLITE_ERROR)
225     r = [NSString stringWithUTF8String:sqlite3_errmsg(self->_connection)];
226   else if (rc == SQLITE_MISUSE)
227     r = @"The SQLite step function was called in an incorrect way";
228   else if (rc == SQLITE_BUSY)
229     r = @"The SQLite is busy.";
230   else
231     r = [NSString stringWithFormat:@"Unexpected SQLite error: %i", rc];
232
233   if ((em = sqlite3_errmsg(self->_connection)) != NULL)
234     r = [r stringByAppendingFormat:@": %s", em];
235   
236   return [SQLiteException exceptionWithName:@"FetchFailed"
237                           reason:r userInfo:nil];
238 }
239
240 - (void)cancelFetch {
241   if (self->statement != NULL) {
242     sqlite3_finalize(self->statement);
243     self->statement = NULL;
244   }
245   self->isDone        = NO;
246   self->hasPendingRow = NO;
247   [super cancelFetch];
248 }
249
250 - (NSArray *)describeResults {
251   // TODO: make exception-less method
252   int                 cnt, fieldCount;
253   NSMutableArray      *result    = nil;
254   NSMutableDictionary *usedNames = nil;
255   NSNumber            *yesObj;
256   
257   yesObj = [NSNumber numberWithBool:YES];
258   
259   if (![self isFetchInProgress]) {
260     [SQLiteException raise:@"NoFetchInProgress"
261                      format:@"No fetch in progress (channel=%@)", self];
262   }
263
264   /* we need to fetch a row to get the info */
265
266   if (!self->hasPendingRow) {
267     NSException *error;
268     
269     if ((error = [self _makeSQLiteStep]) != nil) {
270       [self cancelFetch];
271       [error raise]; // raise error, TODO: make exception-less method
272       return nil;
273     }
274   }
275   if (!self->hasPendingRow) /* no rows available */
276     return nil;
277   
278   fieldCount = sqlite3_column_count(self->statement);
279   
280   /* old code below */
281   
282   result    = [[NSMutableArray      alloc] initWithCapacity:fieldCount];
283   usedNames = [[NSMutableDictionary alloc] initWithCapacity:fieldCount];
284
285   for (cnt = 0; cnt < fieldCount; cnt++) {
286     EOAttribute *attribute  = nil;
287     NSString    *columnName = nil;
288     NSString    *attrName   = nil;
289     
290     columnName = [NSString stringWithCString:
291                              sqlite3_column_name(self->statement, cnt)];
292     attrName   = [columnName _sqlite3ModelMakeInstanceVarName];
293     
294     if ([[usedNames objectForKey:attrName] boolValue]) {
295       int      cnt2 = 0;
296       char     buf[64];
297       NSString *newAttrName = nil;
298
299       for (cnt2 = 2; cnt2 < 100; cnt2++) {
300         NSString *s;
301         sprintf(buf, "%i", cnt2);
302         
303         // TODO: unicode
304         s = [[NSString alloc] initWithCString:buf];
305         newAttrName = [attrName stringByAppendingString:s];
306         [s release];
307         
308         if (![[usedNames objectForKey:newAttrName] boolValue]) {
309           attrName = newAttrName;
310           break;
311         }
312       }
313     }
314     [usedNames setObject:yesObj forKey:attrName];
315
316     attribute = [[EOAttribute alloc] init];
317     [attribute setName:attrName];
318     [attribute setColumnName:columnName];
319     
320     switch (sqlite3_column_type(self->statement, cnt)) {
321       case SQLITE_INTEGER:
322         [attribute setExternalType:@"INTEGER"];
323         [attribute setValueClassName:@"NSNumber"];
324         [attribute setValueType:@"d"];
325         break;
326       case SQLITE_FLOAT:
327         [attribute setExternalType:@"REAL"];
328         [attribute setValueClassName:@"NSNumber"];
329         [attribute setValueType:@"f"];
330         break;
331       case SQLITE_TEXT:
332         [attribute setExternalType:@"TEXT"];
333         [attribute setValueClassName:@"NSString"];
334         break;
335       case SQLITE_BLOB:
336         [attribute setExternalType:@"BLOB"];
337         [attribute setValueClassName:@"NSData"];
338         break;
339       case SQLITE_NULL:
340         NSLog(@"WARNING(%s): got SQLite NULL type at column %i, can't derive "
341               @"type information.",
342               __PRETTY_FUNCTION__, cnt);
343         [attribute setExternalType:@"NULL"];
344         [attribute setValueClassName:@"NSNull"];
345         break;
346       default:
347         NSLog(@"ERROR(%s): unexpected SQLite type at column %i", 
348               __PRETTY_FUNCTION__, cnt);
349         break;
350     }
351     
352     [result addObject:attribute];
353     [attribute release];
354   }
355
356   [usedNames release];
357   usedNames = nil;
358   
359   return [result autorelease];
360 }
361
362 - (BOOL)isColumnNullInCurrentRow:(int)_column {
363   /* 
364      Note: NULL is SQLite is represented as empty strings ..., don't know
365            what to do about that?
366            At least Sybase 10 doesn't support empty strings strings as well
367            and converts them to a single space. So maybe it is reasonable to
368            map empty strings to NSNull?
369            
370            Or is this column-type SQLITE_NULL? If so, thats rather weird,
371            since the type query does not take a row.
372   */
373   return NO;
374 }
375
376 - (NSMutableDictionary *)primaryFetchAttributes:(NSArray *)_attributes
377   withZone:(NSZone *)_zone
378 {
379   /*
380     Note: we expect that the attributes match the generated SQL. This is
381           because auto-generated SQL can contain SQL table prefixes (like
382           alias.column-name which cannot be detected using the attributes
383           schema)
384   */
385   // TODO: add a primaryFetchAttributesX method?
386   NSMutableDictionary *row = nil;
387   NSException *error;
388   unsigned attrCount = [_attributes count];
389   unsigned cnt;
390   
391   if (self->statement == NULL) {
392     NSLog(@"ERROR: no fetch in progress?");
393     [self cancelFetch];
394     return nil;
395   }
396   
397   if (!self->hasPendingRow && !self->isDone) {
398     if ((error = [self _makeSQLiteStep]) != nil) {
399       [self cancelFetch];
400       [error raise]; // raise error, TODO: make exception-less method
401       return nil;
402     }
403   }
404   if (self->isDone) { /* step was fine, but we are at the end */
405     [self cancelFetch];
406     return nil;
407   }
408   
409   self->hasPendingRow = NO; /* consume the row */
410   
411   /* build row */
412   
413   row = [NSMutableDictionary dictionaryWithCapacity:attrCount];
414
415   for (cnt = 0; cnt < attrCount; cnt++) {
416     EOAttribute *attribute;
417     NSString    *attrName;
418     id          value      = nil;
419     
420     attribute = [_attributes objectAtIndex:cnt];
421     attrName  = [attribute name];
422
423     if ([self isColumnNullInCurrentRow:cnt]) {
424       value = [null retain];
425     }
426     else {
427       Class valueClass;
428       
429       valueClass = NSClassFromString([attribute valueClassName]);
430       if (valueClass == Nil) {
431         NSLog(@"ERROR(%s): %@: got no value class for column:\n"
432               @"  attribute=%@\n  type=%@",
433               __PRETTY_FUNCTION__, self,
434               attrName, [attribute externalType]);
435         value = null;
436         continue;
437       }
438       
439       switch (sqlite3_column_type(self->statement, cnt)) {
440       case SQLITE_INTEGER:
441         value = [[valueClass alloc] 
442                   initWithSQLiteInt:sqlite3_column_int(self->statement, cnt)];
443         break;
444       case SQLITE_FLOAT:
445         value = [[valueClass alloc] 
446                   initWithSQLiteDouble:
447                     sqlite3_column_double(self->statement, cnt)];
448         break;
449       case SQLITE_TEXT:
450         value = [[valueClass alloc] 
451                   initWithSQLiteText:
452                     sqlite3_column_text(self->statement, cnt)];
453         break;
454       case SQLITE_BLOB:
455         value = [[valueClass alloc] 
456                   initWithSQLiteData:
457                     sqlite3_column_blob(self->statement, cnt)
458                   length:sqlite3_column_bytes(self->statement, cnt)];
459         break;
460       case SQLITE_NULL:
461         value = [null retain];
462         break;
463       default:
464         NSLog(@"ERROR(%s): unexpected SQLite type at column %i", 
465               __PRETTY_FUNCTION__, cnt);
466         continue;
467       }
468       
469       if (value == nil) {
470         NSLog(@"ERROR(%s): %@: got no value for column:\n"
471               @"  attribute=%@\n  valueClass=%@\n  type=%@",
472               __PRETTY_FUNCTION__, self,
473               attrName, NSStringFromClass(valueClass), 
474               [attribute externalType]);
475         continue;
476       }
477     }
478     
479     if (value != nil) {
480       [row setObject:value forKey:attrName];
481       [value release];
482     }
483   }
484   
485   return row;
486 }
487
488 /* sending SQL to server */
489
490 - (NSException *)evaluateExpressionX:(NSString *)_expression {
491   NSMutableString *sql;
492   NSException *error;
493   BOOL       result;
494   const char *s;
495   const char *tails = NULL;
496   int  rc;
497
498   *(&result) = YES;
499   
500   if (_expression == nil) {
501     [NSException raise:@"InvalidArgumentException"
502                  format:@"parameter for evaluateExpression: "
503                         @"must not be null (channel=%@)", self];
504   }
505   
506   sql = [[_expression mutableCopy] autorelease];
507   [sql appendString:@";"];
508
509   /* ask delegate */
510   
511   if (delegateRespondsTo.willEvaluateExpression) {
512     EODelegateResponse response;
513     
514     response = [delegate adaptorChannel:self willEvaluateExpression:sql];
515     
516     if (response == EODelegateRejects) {
517       return [NSException exceptionWithName:@"EODelegateRejects"
518                           reason:@"delegate rejected insert"
519                           userInfo:nil];
520     }
521     if (response == EODelegateOverrides)
522       return nil;
523   }
524
525   /* check some preconditions */
526   
527   if (![self isOpen]) {
528     return [SQLiteException exceptionWithName:@"ChannelNotOpenException"
529                             reason:@"SQLite connection is not open"
530                             userInfo:nil];
531   }
532   if (self->statement != NULL) {
533     return [SQLiteException exceptionWithName:@"CommandInProgressException"
534                             reason:@"an evaluation is in progress"
535                             userInfo:nil];
536     return NO;
537   }
538   
539   if ([self isFetchInProgress]) {
540     NSLog(@"WARNING: a fetch is still in progress: %@", self);
541     [self cancelFetch];
542   }
543   
544   if (isDebuggingEnabled)
545     NSLog(@"%@ SQL: %@", self, sql);
546
547   /* reset environment */
548   
549   self->isFetchInProgress = NO;
550   self->isDone        = NO;
551   self->hasPendingRow = NO;
552   
553   s  = [sql UTF8String];
554   rc = sqlite3_prepare(self->_connection, s, strlen(s), 
555                        (void *)&(self->statement), &tails);
556   
557   if (rc != SQLITE_OK) {
558     NSString *r;
559     
560     [self cancelFetch];
561     // TODO: improve error
562
563     r = [NSString stringWithFormat:@"could not parse SQL statement: %s",
564                   sqlite3_errmsg(self->_connection)];
565     return [SQLiteException exceptionWithName:@"ExecutionFailed" 
566                             reason:r userInfo:nil];
567   }
568   
569   /* step to first row */
570   
571   if ([sql hasPrefix:@"SELECT"] || [sql hasPrefix:@"select"]) {
572     self->isFetchInProgress = YES;
573     NSAssert(self->statement, @"missing statement");
574   }
575   else {
576     if ((error = [self _makeSQLiteStep]) != nil) {
577       [self cancelFetch];
578       return error;
579     }
580   
581     self->isFetchInProgress = self->hasPendingRow;
582     if (!self->isFetchInProgress) {
583       sqlite3_finalize(self->statement); 
584       self->statement = NULL;
585     }
586   }
587   
588   /* only on empty results? */
589   if (delegateRespondsTo.didEvaluateExpression)
590     [delegate adaptorChannel:self didEvaluateExpression:sql];
591   
592   return nil /* everything is OK */;
593 }
594 - (BOOL)evaluateExpression:(NSString *)_sql {
595   NSException *e;
596   NSString *n;
597   
598   if ((e = [self evaluateExpressionX:_sql]) == nil)
599     return YES;
600   
601   /* for compatibility with non-X methods, translate some errors to a bool */
602   n = [e name];
603   if ([n isEqualToString:@"EOEvaluationError"])
604     return NO;
605   if ([n isEqualToString:@"EODelegateRejects"])
606     return NO;
607   
608   [e raise];
609   return NO;
610 }
611
612 /* description */
613
614 - (NSString *)description {
615   NSMutableString *ms;
616
617   ms = [NSMutableString stringWithCapacity:64];
618   [ms appendFormat:@"<%@[0x%08X] connection=0x%08X",
619                      NSStringFromClass([self class]),
620                      self,
621                      (unsigned)self->_connection];
622   [ms appendString:@">"];
623   return ms;
624 }
625
626 @end /* SQLiteChannel */
627
628 @implementation SQLiteChannel(PrimaryKeyGeneration)
629
630 - (NSDictionary *)primaryKeyForNewRowWithEntity:(EOEntity *)_entity {
631   NSArray       *pkeys;
632   SQLiteAdaptor *adaptor;
633   NSString      *seqName, *seq;
634   NSDictionary  *pkey;
635
636   pkeys   = [_entity primaryKeyAttributeNames];
637   adaptor = (id)[[self adaptorContext] adaptor];
638   seqName = [adaptor primaryKeySequenceName];
639   pkey    = nil;
640   seq     = nil;
641   
642   seq = ([seqName length] > 0)
643     ? [NSString stringWithFormat:@"SELECT NEXTVAL ('%@')", seqName]
644     : [adaptor newKeyExpression];
645   
646   NS_DURING {
647     if ([self evaluateExpression:seq]) {
648       id key = nil;
649       
650       NSLog(@"ERROR: new key creation is not implemented in SQLite yet!");
651       if ([self isFetchInProgress]) {
652         NSLog(@"Primary key eval returned results ..");
653       }
654       // TODO
655       NSLog(@"%s: PKEY GEN NOT IMPLEMENTED!", __PRETTY_FUNCTION__);
656       [self cancelFetch];
657
658       if (key != nil) {
659         pkey = [NSDictionary dictionaryWithObject:key
660                              forKey:[pkeys objectAtIndex:0]];
661       }
662     }
663   }
664   NS_HANDLER {
665     pkey = nil;
666   }
667   NS_ENDHANDLER;
668
669   return pkey;
670 }
671
672 @end /* SQLiteChannel(PrimaryKeyGeneration) */
673
674 void __link_SQLiteChannel() {
675   // used to force linking of object file
676   __link_SQLiteChannel();
677 }