]> err.no Git - sope/blob - sope-gdl1/GDLContentStore/GCSFolder.m
added OGoContentStore library to sope-gdl1
[sope] / sope-gdl1 / GDLContentStore / GCSFolder.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #include "GCSFolder.h"
23 #include "GCSFolderManager.h"
24 #include "GCSFolderType.h"
25 #include "GCSChannelManager.h"
26 #include "GCSFieldExtractor.h"
27 #include "NSURL+GCS.h"
28 #include "EOAdaptorChannel+GCS.h"
29 #include "EOQualifier+GCS.h"
30 #include "GCSStringFormatter.h"
31 #include "common.h"
32
33 @implementation GCSFolder
34
35 static BOOL debugOn    = NO;
36 static BOOL doLogStore = NO;
37
38 static Class NSStringClass       = Nil;
39 static Class NSNumberClass       = Nil;
40 static Class NSCalendarDateClass = Nil;
41
42 static GCSStringFormatter *stringFormatter = nil;
43
44 + (void)initialize {
45   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
46   
47   debugOn         = [ud boolForKey:@"GCSFolderDebugEnabled"];
48   doLogStore      = [ud boolForKey:@"GCSFolderStoreDebugEnabled"];
49
50   NSStringClass       = [NSString class];
51   NSNumberClass       = [NSNumber class];
52   NSCalendarDateClass = [NSCalendarDate class];
53
54   stringFormatter = [GCSStringFormatter sharedFormatter];
55 }
56
57 - (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId
58   folderTypeName:(NSString *)_ftname folderType:(GCSFolderType *)_ftype
59   location:(NSURL *)_loc quickLocation:(NSURL *)_qloc
60   folderManager:(GCSFolderManager *)_fm
61 {
62   if (![_loc isNotNull]) {
63     [self logWithFormat:@"ERROR(%s): missing quicktable parameter!", 
64           __PRETTY_FUNCTION__];
65     [self release];
66     return nil;
67   }
68   
69   if ((self = [super init])) {
70     self->folderManager  = [_fm    retain];
71     self->folderInfo     = [_ftype retain];
72     
73     self->folderId       = [_folderId copy];
74     self->folderName     = [[_path lastPathComponent] copy];
75     self->path           = [_path   copy];
76     self->location       = [_loc    retain];
77     self->quickLocation  = _qloc ? [_qloc   retain] : [_loc retain];
78     self->folderTypeName = [_ftname copy];
79     
80     self->ofFlags.requiresFolderSelect = 0;
81     self->ofFlags.sameTableForQuick = 
82       [self->location isEqualTo:self->quickLocation] ? 1 : 0;
83   }
84   return self;
85 }
86 - (id)init {
87   return [self initWithPath:nil primaryKey:nil
88                folderTypeName:nil folderType:nil 
89                location:nil quickLocation:nil
90                folderManager:nil];
91 }
92
93 - (void)dealloc {
94   [self->folderManager  release];
95   [self->folderInfo     release];
96   [self->folderId       release];
97   [self->folderName     release];
98   [self->path           release];
99   [self->location       release];
100   [self->quickLocation  release];
101   [self->folderTypeName release];
102   [super dealloc];
103 }
104
105 /* accessors */
106
107 - (NSNumber *)folderId {
108   return self->folderId;
109 }
110
111 - (NSString *)folderName {
112   return self->folderName;
113 }
114 - (NSString *)path {
115   return self->path;
116 }
117
118 - (NSURL *)location {
119   return self->location;
120 }
121 - (NSURL *)quickLocation {
122   return self->quickLocation;
123 }
124
125 - (NSString *)folderTypeName {
126   return self->folderTypeName;
127 }
128
129 - (GCSFolderManager *)folderManager {
130   return self->folderManager;
131 }
132 - (GCSChannelManager *)channelManager {
133   return [[self folderManager] channelManager];
134 }
135
136 - (NSString *)storeTableName {
137   return [[self location] gcsTableName];
138 }
139 - (NSString *)quickTableName {
140   return [[self quickLocation] gcsTableName];
141 }
142
143 - (BOOL)isQuickInfoStoredInContentTable {
144   return self->ofFlags.sameTableForQuick ? YES : NO;
145 }
146
147 /* channels */
148
149 - (EOAdaptorChannel *)acquireStoreChannel {
150   return [[self channelManager] acquireOpenChannelForURL:[self location]];
151 }
152 - (EOAdaptorChannel *)acquireQuickChannel {
153   return [[self channelManager] acquireOpenChannelForURL:[self quickLocation]];
154 }
155
156 - (void)releaseChannel:(EOAdaptorChannel *)_channel {
157   [[self channelManager] releaseChannel:_channel];
158   if (debugOn) [self debugWithFormat:@"released channel: %@", _channel];
159 }
160
161 - (BOOL)canConnectStore {
162   return [[self channelManager] canConnect:[self location]];
163 }
164 - (BOOL)canConnectQuick {
165   return [[self channelManager] canConnect:[self quickLocation]];
166 }
167
168 /* operations */
169
170 - (NSArray *)subFolderNames {
171   return [[self folderManager] listSubFoldersAtPath:[self path]
172                                recursive:NO];
173 }
174 - (NSArray *)allSubFolderNames {
175   return [[self folderManager] listSubFoldersAtPath:[self path]
176                                recursive:YES];
177 }
178
179 - (id)_fetchValueOfColumn:(NSString *)_col attributeName:(NSString *)_attrName
180   inContentWithName:(NSString *)_name 
181 {
182   EOAdaptorChannel *channel;
183   NSException  *error;
184   NSDictionary *row;
185   NSArray      *attrs;
186   NSString     *result;
187   NSString     *sql;
188   
189   if ((channel = [self acquireStoreChannel]) == nil) {
190     [self logWithFormat:@"ERROR(%s): could not open storage channel!",
191             __PRETTY_FUNCTION__];
192     return nil;
193   }
194   
195   /* generate SQL */
196   
197   sql = @"SELECT ";
198   sql = [sql stringByAppendingString:_col];
199   sql = [sql stringByAppendingString:@" FROM "];
200   sql = [sql stringByAppendingString:[self storeTableName]];
201   sql = [sql stringByAppendingString:@" WHERE \"c_name\" = '"];
202   sql = [sql stringByAppendingString:_name];
203   sql = [sql stringByAppendingString:@"'"];
204   
205   /* run SQL */
206   
207   if ((error = [channel evaluateExpressionX:sql]) != nil) {
208     [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", 
209             __PRETTY_FUNCTION__, sql, error];
210     [self releaseChannel:channel];
211     return nil;
212   }
213   
214   /* fetch results */
215   
216   result = nil;
217   attrs  = [channel describeResults];
218   if ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
219     result = [[[row objectForKey:_attrName] copy] autorelease];
220     if (![result isNotNull]) result = nil;
221     [channel cancelFetch];
222   }
223   
224   /* release and return result */
225   
226   [self releaseChannel:channel];
227   return result;
228 }
229
230 - (NSNumber *)versionOfContentWithName:(NSString *)_name {
231   return [self _fetchValueOfColumn:@"c_version" attributeName:@"cVersion"
232                inContentWithName:_name];
233 }
234
235 - (NSString *)fetchContentWithName:(NSString *)_name {
236   return [self _fetchValueOfColumn:@"c_content" attributeName:@"cContent"
237                inContentWithName:_name];
238 }
239
240 - (NSDictionary *)fetchContentsOfAllFiles {
241   /*
242     Note: try to avoid the use of this method! The key of the dictionary
243           will be filename, the value the content.
244   */
245   NSMutableDictionary *result;
246   EOAdaptorChannel *channel;
247   NSException  *error;
248   NSDictionary *row;
249   NSArray      *attrs;
250   NSString     *sql;
251   
252   if ((channel = [self acquireStoreChannel]) == nil) {
253     [self logWithFormat:@"ERROR(%s): could not open storage channel!",
254             __PRETTY_FUNCTION__];
255     return nil;
256   }
257   
258   /* generate SQL */
259   
260   sql = @"SELECT \"c_name\", \"c_content\" FROM ";
261   sql = [sql stringByAppendingString:[self storeTableName]];
262   
263   /* run SQL */
264   
265   if ((error = [channel evaluateExpressionX:sql]) != nil) {
266     [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", 
267             __PRETTY_FUNCTION__, sql, error];
268     [self releaseChannel:channel];
269     return nil;
270   }
271   
272   /* fetch results */
273   
274   result = [NSMutableDictionary dictionaryWithCapacity:128];
275   attrs  = [channel describeResults];
276   while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
277     NSString *cName, *cContent;
278
279     cName    = [row objectForKey:@"cName"];
280     cContent = [row objectForKey:@"cContent"];
281     
282     if (![cName isNotNull]) {
283       [self logWithFormat:@"ERROR: missing cName in row: %@", row];
284       continue;
285     }
286     if (![cContent isNotNull]) {
287       [self logWithFormat:@"ERROR: missing cContent in row: %@", row];
288       continue;
289     }
290     
291     [result setObject:cContent forKey:cName];
292   }
293   
294   /* release and return result */
295   
296   [self releaseChannel:channel];
297   return result;
298 }
299
300 /* writing content */
301
302 - (NSString *)_formatRowValue:(id)_value {
303   if (![_value isNotNull])
304     return @"NULL";
305
306   if ([_value isKindOfClass:NSStringClass])
307     return [stringFormatter stringByFormattingString:_value];
308
309   if ([_value isKindOfClass:NSNumberClass])
310     return [_value stringValue];
311
312   if ([_value isKindOfClass:NSCalendarDateClass]) {
313     /* be smart ... convert to timestamp */
314     return [NSString stringWithFormat:@"%i", [_value timeIntervalSince1970]];
315   }
316   
317   [self logWithFormat:@"cannot handle value class: %@", [_value class]];
318   return nil;
319 }
320
321 - (NSString *)_generateInsertStatementForRow:(NSDictionary *)_row
322   tableName:(NSString *)_table
323 {
324   // TODO: move to NSDictionary category?
325   NSMutableString *sql;
326   NSArray  *keys;
327   unsigned i, count;
328   
329   if (_row == nil || _table == nil)
330     return nil;
331   
332   keys = [_row allKeys];
333   
334   sql = [NSMutableString stringWithCapacity:512];
335   [sql appendString:@"INSERT INTO "];
336   [sql appendString:_table];
337   [sql appendString:@" ("];
338   
339   for (i = 0, count = [keys count]; i < count; i++) {
340     if (i != 0) [sql appendString:@", "];
341     [sql appendString:[keys objectAtIndex:i]];
342   }
343   
344   [sql appendString:@") VALUES ("];
345   
346   for (i = 0, count = [keys count]; i < count; i++) {
347     id value;
348     
349     if (i != 0) [sql appendString:@", "];
350     value = [_row objectForKey:[keys objectAtIndex:i]];
351     value = [self _formatRowValue:value];
352     [sql appendString:value];
353   }
354   
355   [sql appendString:@")"];
356   return sql;
357 }
358
359 - (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row
360   tableName:(NSString *)_table
361   whereColumn:(NSString *)_colname isEqualTo:(id)_value
362 {
363   // TODO: move to NSDictionary category?
364   NSMutableString *sql;
365   NSArray  *keys;
366   unsigned i, count;
367   
368   if (_row == nil || _table == nil)
369     return nil;
370   
371   keys = [_row allKeys];
372   
373   sql = [NSMutableString stringWithCapacity:512];
374   [sql appendString:@"UPDATE "];
375   [sql appendString:_table];
376
377   [sql appendString:@" SET "];
378   for (i = 0, count = [keys count]; i < count; i++) {
379     id value;
380
381     value = [_row objectForKey:[keys objectAtIndex:i]];
382     value = [self _formatRowValue:value];
383     
384     if (i != 0) [sql appendString:@", "];
385     [sql appendString:[keys objectAtIndex:i]];
386     [sql appendString:@" = "];
387     [sql appendString:value];
388   }
389   
390   [sql appendString:@" WHERE "];
391   [sql appendString:_colname];
392   [sql appendString:@" = "];
393   [sql appendString:[self _formatRowValue:_value]];
394   
395   return sql;
396 }
397
398 - (NSException *)writeContent:(NSString *)_content toName:(NSString *)_name {
399   EOAdaptorChannel    *storeChannel, *quickChannel;
400   NSMutableDictionary *quickRow, *contentRow;
401   GCSFieldExtractor   *extractor;
402   NSException         *error;
403   NSNumber            *storedVersion;
404   BOOL                isNewRecord;
405   NSCalendarDate      *nowDate;
406   NSNumber            *now;
407   NSString            *qsql, *bsql;
408
409   /* check preconditions */
410   
411   if (_name == nil) {
412     return [NSException exceptionWithName:@"GCSStoreException"
413                         reason:@"no content filename was provided"
414                         userInfo:nil];
415   }
416   if (_content == nil) {
417     return [NSException exceptionWithName:@"GCSStoreException"
418                         reason:@"no content was provided"
419                         userInfo:nil];
420   }
421   
422   /* run */
423
424   error   = nil;
425   nowDate = [NSCalendarDate date];
426   now     = [NSNumber numberWithUnsignedInt:[nowDate timeIntervalSince1970]];
427   
428   if (doLogStore)
429     [self logWithFormat:@"should store content: '%@'\n%@", _name, _content];
430   
431   storedVersion = [self versionOfContentWithName:_name];
432   if (doLogStore)
433     [self logWithFormat:@"  version: %@", storedVersion];
434   isNewRecord = [storedVersion isNotNull] ? NO : YES;
435   
436   /* extract quick info */
437   
438   extractor = [self->folderInfo quickExtractor];
439   quickRow  = [extractor extractQuickFieldsFromContent:_content];
440   [quickRow setObject:_name forKey:@"c_name"];
441   
442   if (doLogStore)
443     [self logWithFormat:@"  store quick: %@", quickRow];
444   
445   /* make content row */
446   
447   contentRow = [NSMutableDictionary dictionaryWithCapacity:16];
448   
449   if (self->ofFlags.sameTableForQuick)
450     [contentRow addEntriesFromDictionary:quickRow];
451   
452   [contentRow setObject:_name forKey:@"c_name"];
453   if (isNewRecord) [contentRow setObject:now forKey:@"c_creationdate"];
454   [contentRow setObject:now forKey:@"c_lastmodified"];
455   if (isNewRecord)
456     [contentRow setObject:[NSNumber numberWithInt:0] forKey:@"c_version"];
457   else {
458     // TODO: increase version?
459     [contentRow setObject:[NSNumber numberWithInt:[storedVersion intValue]]
460                 forKey:@"c_version"];
461   }
462   [contentRow setObject:_content forKey:@"c_content"];
463   
464   /* open channels */
465   
466   if ((storeChannel = [self acquireStoreChannel]) == nil) {
467     [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
468     return nil;
469   }
470   if (!self->ofFlags.sameTableForQuick) {
471     if ((quickChannel = [self acquireQuickChannel]) == nil) {
472       [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
473       [self releaseChannel:storeChannel];
474       return nil;
475     }
476   }
477
478   /* generate SQL */
479   
480   qsql = nil;
481   if (isNewRecord) { /* insert */
482     if (!self->ofFlags.sameTableForQuick) {
483       qsql = [self _generateInsertStatementForRow:quickRow 
484                    tableName:[self quickTableName]];
485     }
486     bsql = [self _generateInsertStatementForRow:contentRow
487                  tableName:[self storeTableName]];
488   }
489   else {
490     if (!self->ofFlags.sameTableForQuick) {
491       qsql = [self _generateUpdateStatementForRow:quickRow
492                    tableName:[self quickTableName]
493                    whereColumn:@"c_name" isEqualTo:_name];
494     }
495     bsql = [self _generateUpdateStatementForRow:contentRow
496                  tableName:[self storeTableName]
497                  whereColumn:@"c_name" isEqualTo:_name];
498   }
499   
500   /* execute */
501   // TODO: execute in transactions
502
503   if ((error = [storeChannel evaluateExpressionX:bsql]) != nil) {
504     [self logWithFormat:@"ERROR(%s): cannot %s content '%@': %@", 
505           __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update", bsql, error];
506   }
507   
508   if (error == nil && qsql != nil) {
509     if ((error = [quickChannel evaluateExpressionX:qsql]) != nil) {
510       NSString    *delsql;
511       NSException *delErr;
512         
513       [self logWithFormat:@"ERROR(%s): cannot %s quick '%@': %@", 
514               __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update", 
515               qsql, error];
516       
517       if (isNewRecord) {
518         /* insert in quick failed, so delete in content table */
519         
520         delsql = [@"DELETE FROM " stringByAppendingString:
521                      [self storeTableName]];
522         delsql = [delsql stringByAppendingString:@" WHERE c_name="];
523         delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
524         if ((delErr = [storeChannel evaluateExpressionX:delsql]) != nil) {
525             [self logWithFormat:
526                   @"ERROR(%s): could not delete content '%@' after quick-fail:"
527                   @" %@", __PRETTY_FUNCTION__, delsql, error];
528         }
529       }
530     }
531   }
532   
533   [self releaseChannel:storeChannel];
534   if (!self->ofFlags.sameTableForQuick) [self releaseChannel:quickChannel];
535   return error;
536 }
537
538 - (NSException *)deleteContentWithName:(NSString *)_name {
539   EOAdaptorChannel *storeChannel, *quickChannel;
540   NSException *error;
541   NSString *delsql;
542   
543   /* check preconditions */
544   
545   if (_name == nil) {
546     return [NSException exceptionWithName:@"GCSDeleteException"
547                         reason:@"no content filename was provided"
548                         userInfo:nil];
549   }
550   
551   if (doLogStore)
552     [self logWithFormat:@"should delete content: '%@'", _name];
553   
554   /* open channels */
555   
556   if ((storeChannel = [self acquireStoreChannel]) == nil) {
557     [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
558     return nil;
559   }
560   if (!self->ofFlags.sameTableForQuick) {
561     if ((quickChannel = [self acquireQuickChannel]) == nil) {
562       [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
563       [self releaseChannel:storeChannel];
564       return nil;
565     }
566   }
567   
568   /* delete rows */
569
570   delsql = [@"DELETE FROM " stringByAppendingString:[self storeTableName]];
571   delsql = [delsql stringByAppendingString:@" WHERE c_name="];
572   delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
573   if ((error = [storeChannel evaluateExpressionX:delsql]) != nil) {
574     [self logWithFormat:
575             @"ERROR(%s): cannot delete content '%@': %@", 
576           __PRETTY_FUNCTION__, delsql, error];
577   }
578   else if (!self->ofFlags.sameTableForQuick) {
579     /* content row deleted, now delete the quick row */
580     delsql = [@"DELETE FROM " stringByAppendingString:[self quickTableName]];
581     delsql = [delsql stringByAppendingString:@" WHERE c_name="];
582     delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
583     if ((error = [quickChannel evaluateExpressionX:delsql]) != nil) {
584       [self logWithFormat:
585               @"ERROR(%s): cannot delete quick row '%@': %@", 
586             __PRETTY_FUNCTION__, delsql, error];
587       /* 
588          Note: we now have a "broken" record, needs to be periodically GCed by
589                a script!
590       */
591     }
592   }
593   
594   /* release channels and return */
595   
596   [self releaseChannel:storeChannel];
597   if (!self->ofFlags.sameTableForQuick)
598     [self releaseChannel:quickChannel];
599   return error;
600 }
601
602 - (NSString *)columnNameForFieldName:(NSString *)_fieldName {
603   return _fieldName;
604 }
605
606 /* SQL generation */
607
608 - (NSString *)generateSQLForSortOrderings:(NSArray *)_so {
609   NSMutableString *sql;
610   unsigned i, count;
611
612   if ((count = [_so count]) == 0)
613     return nil;
614   
615   sql = [NSMutableString stringWithCapacity:(count * 16)];
616   for (i = 0; i < count; i++) {
617     EOSortOrdering *so;
618     NSString *column;
619     SEL      sel;
620     
621     so     = [_so objectAtIndex:i];
622     sel    = [so selector];
623     column = [self columnNameForFieldName:[so key]];
624     
625     if (i > 0) [sql appendString:@", "];
626     
627     if (sel_eq(sel, EOCompareAscending)) {
628       [sql appendString:column];
629       [sql appendString:@" ASC"];
630     }
631     else if (sel_eq(sel, EOCompareDescending)) {
632       [sql appendString:column];
633       [sql appendString:@" DESC"];
634     }
635     else if (sel_eq(sel, EOCompareCaseInsensitiveAscending)) {
636       [sql appendString:@"UPPER("];
637       [sql appendString:column];
638       [sql appendString:@") ASC"];
639     }
640     else if (sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
641       [sql appendString:@"UPPER("];
642       [sql appendString:column];
643       [sql appendString:@") DESC"];
644     }
645     else {
646       [self logWithFormat:@"cannot handle sort selector in store: %@",
647               NSStringFromSelector(sel)];
648     }
649   }
650   return sql;
651 }
652
653 - (NSString *)generateSQLForQualifier:(EOQualifier *)_q {
654   NSMutableString *ms;
655   
656   if (_q == nil) return nil;
657   ms = [NSMutableString stringWithCapacity:32];
658   [_q _gcsAppendToString:ms];
659   return ms;
660 }
661
662 /* fetching */
663
664 - (NSArray *)fetchFields:(NSArray *)_flds 
665   fetchSpecification:(EOFetchSpecification *)_fs
666 {
667   EOQualifier      *qualifier;
668   NSArray          *sortOrderings;
669   EOAdaptorChannel *channel;
670   NSException      *error;
671   NSMutableString  *sql;
672   NSArray          *attrs;
673   NSMutableArray   *results;
674   NSDictionary     *row;
675   
676   qualifier     = [_fs qualifier];
677   sortOrderings = [_fs sortOrderings];
678   
679 #if 0
680   [self logWithFormat:@"FETCH: %@", _flds];
681   [self logWithFormat:@"  MATCH: %@", _q];
682 #endif
683   
684   /* generate SQL */
685
686   sql = [NSMutableString stringWithCapacity:256];
687   [sql appendString:@"SELECT "];
688   if (_flds == nil)
689     [sql appendString:@"*"];
690   else {
691     unsigned i, count;
692     
693     count = [_flds count];
694     for (i = 0; i < count; i++) {
695       if (i > 0) [sql appendString:@", "];
696       [sql appendString:[self columnNameForFieldName:[_flds objectAtIndex:i]]];
697     }
698   }
699   [sql appendString:@" FROM "];
700   [sql appendString:[self quickTableName]];
701   
702   if (qualifier != nil) {
703     [sql appendString:@" WHERE "];
704     [sql appendString:[self generateSQLForQualifier:qualifier]];
705   }
706   if ([sortOrderings count] > 0) {
707     [sql appendString:@" ORDER BY "];
708     [sql appendString:[self generateSQLForSortOrderings:sortOrderings]];
709   }
710 #if 0
711   /* limit */
712   [sql appendString:@" LIMIT "]; // count
713   [sql appendString:@" OFFSET "]; // index from 0
714 #endif
715   
716   /* open channel */
717
718   if ((channel = [self acquireStoreChannel]) == nil) {
719     [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
720     return nil;
721   }
722   
723   /* run SQL */
724
725   if ((error = [channel evaluateExpressionX:sql]) != nil) {
726     [self logWithFormat:@"ERROR(%s): cannot execute quick-fetch SQL '%@': %@", 
727             __PRETTY_FUNCTION__, sql, error];
728     [self releaseChannel:channel];
729     return nil;
730   }
731
732   /* fetch results */
733   
734   results = [NSMutableArray arrayWithCapacity:64];
735   attrs   = [channel describeResults];
736   while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
737     [results addObject:row];
738   
739   /* release channels */
740   
741   [self releaseChannel:channel];
742   
743   return results;
744 }
745 - (NSArray *)fetchFields:(NSArray *)_flds matchingQualifier:(EOQualifier *)_q {
746   EOFetchSpecification *fs;
747
748   if (_q == nil)
749     fs = nil;
750   else {
751     fs = [EOFetchSpecification fetchSpecificationWithEntityName:
752                                  [self folderName]
753                                qualifier:_q
754                                sortOrderings:nil];
755   }
756   return [self fetchFields:_flds fetchSpecification:fs];
757 }
758
759 /* description */
760
761 - (NSString *)description {
762   NSMutableString *ms;
763   id tmp;
764   
765   ms = [NSMutableString stringWithCapacity:256];
766   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
767
768   if (self->folderId)
769     [ms appendFormat:@" id=%@", self->folderId];
770   else
771     [ms appendString:@" no-id"];
772
773   if ((tmp = [self path]))           [ms appendFormat:@" path=%@", tmp];
774   if ((tmp = [self folderTypeName])) [ms appendFormat:@" type=%@", tmp];
775   if ((tmp = [self location]))
776     [ms appendFormat:@" loc=%@", [tmp absoluteString]];
777   
778   [ms appendString:@">"];
779   return ms;
780 }
781
782 @end /* GCSFolder */