]> err.no Git - sope/blob - sope-appserver/NGObjWeb/WebDAV/SoDAVSQLParser.m
fixed WOContext superclass version
[sope] / sope-appserver / NGObjWeb / WebDAV / SoDAVSQLParser.m
1 /*
2   Copyright (C) 2002-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
6   SOPE 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   SOPE 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 SOPE; 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 "SoDAVSQLParser.h"
23 #include <EOControl/EOQualifier.h>
24 #include <EOControl/EOFetchSpecification.h>
25 #include <EOControl/EOSortOrdering.h>
26 #include <EOControl/EOClassDescription.h>
27 #include "common.h"
28
29 // TODO: better error output
30
31 @interface SoDAVSQLParser(Logging) /* this is available in NGExtensions */
32 - (void)logWithFormat:(NSString *)_fmt,...;
33 @end
34
35 @interface EOSQLQualifierIdMapper : NSObject
36 @end
37
38 @implementation SoDAVSQLParser
39
40 + (id)sharedSQLParser {
41   static SoDAVSQLParser *sharedParser = nil; // THREAD
42   if (sharedParser == nil)
43     sharedParser = [[SoDAVSQLParser alloc] init];
44   return sharedParser;
45 }
46
47 - (void)dealloc {
48   [super dealloc];
49 }
50
51 /* top level parsers */
52
53 - (EOFetchSpecification *)parseSQLSelectStatement:(NSString *)_sql {
54   EOFetchSpecification *fs;
55   unichar  *us, *pos;
56   unsigned len, remainingLen;
57   
58   if ((len = [_sql length]) == 0) return nil;
59
60   us  = calloc(len + 10, sizeof(unichar));
61   [_sql getCharacters:us];
62   us[len] = 0;
63   pos = us;
64   remainingLen = len;
65   
66   if (![self parseSQL:&fs from:&pos length:&remainingLen strict:NO])
67     [self logWithFormat:@"parsing of SQL failed."];
68   
69   if (us) free(us);
70   
71   return [fs autorelease];
72 }
73
74 - (EOQualifier *)_parseSQLWhereExpression:(NSString *)_sql {
75   // TODO: process %=>* and %%, and $
76   unichar  *buf;
77   unsigned i, len;
78   BOOL     didReplace;
79   if ((len = [_sql length]) == 0) return nil;
80   
81   // TODO: improve, real parsing in qualifier parser !
82   
83   buf = calloc(len + 3, sizeof(unichar));
84   NSAssert(buf, @"could not allocate char buffer");
85   
86   [_sql getCharacters:buf];
87   for (i = 0, didReplace = NO; i < len; i++) {
88     if (buf[i] != '%') {
89       if (buf[i] == '*') {
90         [self warnWithFormat:@"(%s): SQL string contains a '*': %@",
91                 __PRETTY_FUNCTION__, _sql];
92       }
93       continue;
94     }
95     buf[i] = '%';    
96     didReplace = YES;
97   }
98   if (didReplace)
99     _sql = [NSString stringWithCharacters:buf length:len];
100   if (buf) free(buf);
101   
102   return [EOQualifier qualifierWithQualifierFormat:_sql];
103 }
104
105 /* parsing parts (exported for overloading in subclasses) */
106
107 static inline BOOL
108 uniIsCEq(unichar *haystack, const unsigned char *needle, unsigned len) 
109 {
110   register unsigned idx;
111   for (idx = 0; idx < len; idx++) {
112     if (*needle == '\0')               return YES;
113     if (toupper(haystack[idx]) != needle[idx]) return NO;
114   }
115   return YES;
116 }
117 static inline void skipSpaces(unichar **pos, unsigned *len) {
118   while (*len > 0) {
119     if (!isspace(*pos[0])) return;
120     (*len)--;
121     (*pos)++;
122   }
123 }
124 static void printUniStr(unichar *pos, unsigned len) __attribute__((unused));
125 static void printUniStr(unichar *pos, unsigned len) {
126   unsigned i;
127   for (i = 0; i < len && i < 80; i++)
128     putchar(pos[i]);
129   putchar('\n');
130 }
131
132 static inline BOOL isTokStopChar(unichar c) {
133   switch (c) {
134   case 0:
135   case ')': case '(': case '"': case '\'':
136     return YES;
137   default:
138     if (isspace(c)) return YES;
139     return NO;
140   }
141 }
142
143 - (BOOL)parseToken:(const unsigned char *)tk
144   from:(unichar **)pos length:(unsigned *)len
145   consume:(BOOL)consume
146 {
147   /* ...[space] (strlen(tk)+1 chars) */
148   unichar  *scur;
149   unsigned slen, tlen;
150   
151   tlen = strlen(tk);
152   scur=*pos; slen=*len; // begin transaction
153   skipSpaces(&scur, &slen);
154   
155   if (slen < tlen)
156     return NO;
157   if (toupper(scur[0]) != tk[0])
158     return NO;
159   if (tlen < slen) { /* if tok is not at the end */
160     if (!isTokStopChar(scur[tlen]))
161       return NO; /* not followed by a token stopper */
162   }
163   if (!uniIsCEq(scur, tk, tlen)) 
164     return NO;
165   
166   scur+=tlen; slen-=tlen;
167   
168   if (consume) { *pos = scur; *len = slen; } // end tx
169   return YES;
170 }
171
172 - (BOOL)parseIdentifier:(NSString **)result
173   from:(unichar **)pos length:(unsigned *)len
174   consume:(BOOL)consume
175 {
176   /* "attr" or attr (at least 1 char or 2 for ") */
177   unichar  *scur;
178   unsigned slen;
179   
180   if (result) *result = nil;
181   scur=*pos; slen=*len; // begin transaction
182   skipSpaces(&scur, &slen);
183   
184   if (*scur == '"') {
185     /* quoted attr */
186     unichar *start;
187     
188     //printf("try quoted attr\n");
189     if (slen < 2) return NO;
190     scur++; slen--; /* skip quote */
191     if (*scur == '"') {
192       /* empty name */
193       scur++; slen--;
194       if (consume) { *pos = scur; *len = slen; } // end transaction
195       *result = @"";
196       //printf("is empty quoted\n");
197       return YES;
198     }
199     if (slen < 2) return NO;
200     
201     start = scur;
202     while ((slen > 0) && (*scur != '"')) {
203       if (*scur == '\\' && (slen > 1)) {
204         /* quoted char */
205         scur++; slen--; // skip one more (still needs to be filtered in result
206       }
207       scur++; slen--;
208     }
209     if (slen > 0) { scur++; slen--; } /* skip quote */
210     
211     // TODO: xhandle contained quoted chars ?
212     *result = 
213       [[NSString alloc] initWithCharacters:start length:(scur-start-1)];
214     //NSLog(@"found qattr: %@", *result);
215   }
216   else {
217     /* non-quoted attr */
218     unichar *start;
219     
220     if (slen < 1) return NO;
221     
222     if ([self parseToken:"FROM" from:&scur length:&slen consume:NO]) {
223       /* not an attribute, the from starts ... */
224       // printf("rejected unquoted attr, is a FROM\n");
225       return NO;
226     }
227     if ([self parseToken:"WHERE" from:&scur length:&slen consume:NO]) {
228       /* not an attribute, the where starts ... */
229       // printf("rejected unquoted attr, is a WHERE\n");
230       return NO;
231     }
232     
233     start = scur;
234     while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
235       slen--;
236       scur++;
237     }
238     *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
239     //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
240   }
241   if (consume && result) { *pos = scur; *len = slen; } // end transaction
242   return *result ? YES : NO;
243 }
244
245 - (BOOL)parseTableName:(NSString **)result
246   from:(unichar **)pos length:(unsigned *)len
247   consume:(BOOL)consume
248 {
249   return [self parseIdentifier:result from:pos length:len consume:consume];
250 }
251
252 - (BOOL)parseIdentifierList:(NSArray **)result
253   from:(unichar **)pos length:(unsigned *)len
254   selector:(SEL)_sel
255 {
256   /* attr[,attr] */
257   NSMutableArray *attrs = nil;
258   unichar  *scur;
259   unsigned slen;
260   id       attr;
261   BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
262   
263   if (result) *result = nil;
264   scur=*pos; slen=*len; // begin transaction
265   skipSpaces(&scur, &slen);
266   parser = (void *)[self methodForSelector:_sel];
267   
268   if (slen < 1) return NO; // not enough chars
269   
270   if (*scur == '*') {
271     /* a wildcard list, return 'nil' as result */
272     //printf("try wildcard\n");
273     scur++; slen--; // skip '*'
274     if (!(slen == 0 || isspace(*scur))) {
275       /* not followed by space or at end */
276       return NO;
277     }
278     *pos = scur; *len = slen; // end transaction
279     *result = nil;
280     return YES;
281   }
282   
283   if (!parser(self, _sel, &attr,&scur,&slen,YES))
284     /* well, we need at least one attribute to make it a list */
285     return NO;
286   
287   attrs = [[NSMutableArray alloc] initWithCapacity:32];
288   [attrs addObject:attr]; [attr release];
289   
290   /* all the remaining attributes must be prefixed with a "," */
291   while (slen > 1) {
292     //printf("try next list attr comma\n");
293     skipSpaces(&scur, &slen);
294     if (slen < 2) break;
295     if (*scur != ',') break;
296     scur++; slen--; // skip ','
297     
298     //printf("try next list attr\n");
299     if (!parser(self, _sel, &attr,&scur,&slen,YES))
300       break;
301     
302     [attrs addObject:attr]; [attr release];
303   }
304   
305   *pos = scur; *len = slen; // end transaction
306   *result = attrs;
307   return YES;
308 }
309
310 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
311   from:(unichar **)pos length:(unsigned *)len
312 {
313   /* contains('"hh@"') [12+ chars] */
314   unichar  *scur;
315   unsigned slen;
316   NSString *s;
317   if (q_) *q_ = nil;
318   skipSpaces(&scur, &slen);
319   
320   if (slen < 12) return NO; // not enough chars
321   
322   if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
323     return NO;
324   skipSpaces(&scur, &slen);
325   [self parseToken:"('" from:&scur length:&slen consume:YES];
326   
327   if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
328     return NO;
329   
330   skipSpaces(&scur, &slen);
331   [self parseToken:"')" from:&scur length:&slen consume:YES];
332   
333   *q_ = [[EOQualifier qualifierWithQualifierFormat:
334                         @"contentAsString doesContain: %@", s] retain];
335   if (*q_) {
336     *pos = scur; *len = slen; // end transaction
337     return YES;
338   }
339   else
340     return NO;
341 }
342
343 - (BOOL)parseQualifier:(EOQualifier **)result
344   from:(unichar **)pos length:(unsigned *)len
345 {
346   unichar  *scur;
347   unsigned slen;
348   
349   if (result) *result = nil;
350   scur=*pos; slen=*len; // begin transaction
351   skipSpaces(&scur, &slen);
352   
353   if (slen < 3) return NO; // not enough chars
354   
355   // for now should scan till we find either ORDER BY order GROUP BY
356   {
357     unichar *start = scur;
358     
359     while (slen > 0) {
360       if (*scur == 'O' || *scur == 'o') {
361         if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
362           //printf("FOUND ORDER TOKEN ...\n");
363           break;
364         }
365       }
366       else if (*scur == 'G' || *scur == 'g') {
367         if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
368           //printf("FOUND GROUP TOKEN ...\n");
369           break;
370         }
371       }
372       
373       scur++; slen--;
374     }
375
376     {
377       EOQualifier *q;
378       NSString *s;
379       
380       s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
381       if ([s length] == 0) {
382         [s release];
383         return NO;
384       }
385       if ((q = [self parseSQLWhereExpression:s]) == nil) {
386         [s release];
387         return NO;
388       }
389       *result = [q retain];
390       [s release];
391     }
392   }
393   
394   *pos = scur; *len = slen; // end transaction
395   return YES;
396 }
397
398 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
399   from:(unichar **)pos length:(unsigned *)len
400 {
401   /* 
402     "('shallow traversal of "..."')"
403     "('hierarchical traversal of "..."')"
404   */
405   unichar  *scur;
406   unsigned slen;
407   NSString *entityName;
408   BOOL isShallow = NO;
409   BOOL isDeep    = NO;
410   
411   if (_scope)  *_scope  = nil;
412   if (_entity) *_entity = nil;
413   scur=*pos; slen=*len; // begin transaction
414   skipSpaces(&scur, &slen);
415   if (slen < 14) return NO; // not enough chars
416   
417   if (*scur != '(') return NO; // does not start with '('
418   scur++; slen--; // skip '('
419   skipSpaces(&scur, &slen);
420   
421   if (*scur != '\'') return NO; // does not start with '(''
422   scur++; slen--; // skip single quote
423   
424   /* next the depth */
425   
426   if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
427     isShallow = YES;
428   else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
429     isDeep = YES;
430   else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
431     isDeep = YES;
432   else
433     /* unknown traveral key */
434     return NO;
435   
436   /* some syntactic sugar (not strict about that ...) */
437   [self parseToken:"TRAVERSAL" from:&scur length:&slen consume:YES];
438   [self parseToken:"OF"        from:&scur length:&slen consume:YES];
439   if (slen < 1) return NO; // not enough chars
440   
441   /* now the entity */
442   skipSpaces(&scur, &slen);
443   if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
444     return NO; // failed to parse entity from scope
445
446   /* trailer */
447   skipSpaces(&scur, &slen);
448   if (slen > 0 && *scur == '\'') {
449     scur++; slen--; // skip single quote
450   }
451   skipSpaces(&scur, &slen);
452   if (slen > 0 && *scur == ')') {
453     scur++; slen--; // skip ')'
454   }
455   
456   if (_scope)  *_scope  = isShallow ? @"flat" : @"deep";
457   if (_entity) *_entity = entityName;
458   *pos = scur; *len = slen; // end transaction
459   return YES;
460 }
461
462 - (BOOL)parseSELECT:(EOFetchSpecification **)result
463   from:(unichar **)pos length:(unsigned *)len
464   strict:(BOOL)beStrict
465 {
466   EOFetchSpecification *fs;
467   NSMutableDictionary *lHints;
468   NSString *scope     = nil;
469   NSArray  *attrs     = nil;
470   NSArray  *fromList  = nil;
471   NSArray  *orderList = nil;
472   NSArray  *lSortOrderings = nil;
473   EOQualifier *q = nil;
474   BOOL hasSelect = NO;
475   BOOL hasFrom   = NO;
476   BOOL missingByOfOrder = NO;
477   BOOL missingByOfGroup = NO;
478   
479   *result = nil;
480   
481   if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
482     /* must begin with SELECT */
483     if (beStrict) return NO;
484   }
485   else
486     hasSelect = YES;
487   
488   if (![self parseIdentifierList:&attrs from:pos length:len
489              selector:@selector(parseColumnName:from:length:consume:)]) {
490     [self logWithFormat:@"missing ID list .."];
491     return NO;
492   }
493   //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
494   
495   /* now a from is expected */
496   if ([self parseToken:"FROM" from:pos length:len consume:YES])
497     hasFrom = YES;
498   else {
499     if (beStrict) return NO;
500   }
501   
502   /* check whether it's followed by a scope */
503   if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
504     NSString *scopeEntity = nil;
505     
506     if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
507       if (beStrict) return NO;
508     }
509 #if DEBUG_PARSING
510     else
511       [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
512 #endif
513     
514     if (scopeEntity)
515       fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
516     [scopeEntity release];
517   }
518   else {
519     if (![self parseIdentifierList:&fromList from:pos length:len
520                selector:@selector(parseTableName:from:length:consume:)]) {
521       [self logWithFormat:@"missing from list .."];
522       return NO;
523     }
524 #if DEBUG_PARSING
525     [self logWithFormat:@"parsed FROM list (%i): %@",
526           [fromList count], fromList];
527 #endif
528   }
529   
530   /* check where */
531   if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
532     /* parse qualifier ... */
533     
534     if ([self parseToken:"CONTAINS" from:pos length:len consume:NO]) {
535       if (![self parseContainsQualifier:&q from:pos length:len]) {
536         if (beStrict) return NO;
537       }
538     }
539     else if (![self parseQualifier:&q from:pos length:len]) {
540       if (beStrict) return NO;
541     }
542 #if DEBUG_PARSING
543     [self logWithFormat:@"FOUND Qualifier: '%@'", q];
544 #endif
545   }
546   
547   /* check order-by */
548   if ([self parseToken:"ORDER" from:pos length:len consume:YES]) {
549     if (![self parseToken:"BY" from:pos length:len consume:YES]) {
550       if (beStrict) return NO;
551       missingByOfOrder = YES;
552     }
553     
554     if (![self parseIdentifierList:&orderList from:pos length:len
555                selector:@selector(parseColumnName:from:length:consume:)])
556       return NO;
557 #if DEBUG_PARSING
558     [self logWithFormat:@"parsed ORDER list (%i): %@", 
559             [orderList count], orderList];
560 #endif
561   }
562   
563   /* check group-by */
564   if ([self parseToken:"GROUP" from:pos length:len consume:YES]) {
565     if (![self parseToken:"BY" from:pos length:len consume:YES]) {
566       if (beStrict) return NO;
567       missingByOfGroup = YES;
568     }
569   }
570   
571   //printUniStr(*pos, *len); // DEBUG
572   
573   if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
574   if (!hasFrom)   [self logWithFormat:@"missing FROM !"];
575   if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
576
577   /* build fetchspec */
578
579   lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
580   
581   if (scope) {
582     [lHints setObject:scope forKey:@"scope"];
583     [scope release]; scope = nil;
584   }
585   if (attrs) {
586     [lHints setObject:attrs forKey:@"attributes"];
587     [attrs release]; attrs = nil;
588   }
589   if (orderList) {
590     NSMutableArray *ma;
591     unsigned i, len;
592     
593     len = [orderList count];
594     ma = [[NSMutableArray alloc] initWithCapacity:len];
595     for (i = 0; i < len; i++) {
596       EOSortOrdering *so;
597       
598       so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
599                            selector:EOCompareAscending];
600     }
601     lSortOrderings = [ma shallowCopy];
602     [ma release];
603     [orderList release]; orderList = nil;
604   }
605   
606   fs = [[EOFetchSpecification alloc]
607          initWithEntityName:[fromList componentsJoinedByString:@","]
608          qualifier:q
609          sortOrderings:lSortOrderings
610          usesDistinct:NO isDeep:NO hints:lHints];
611   [lHints release];
612   [q release];
613   [fromList release];
614   
615   *result = fs;
616   return fs ? YES : NO;
617 }
618
619 - (BOOL)parseSQL:(id *)result
620   from:(unichar **)pos length:(unsigned *)len
621   strict:(BOOL)beStrict
622 {
623   if (*len < 1) return NO;
624   
625   if ([self parseToken:"SELECT" from:pos length:len consume:NO])
626     return [self parseSELECT:result from:pos length:len strict:beStrict];
627   
628   //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
629   //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
630   //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
631   
632   [self logWithFormat:@"tried to parse an unsupported SQL statement."];
633   return NO;
634 }
635
636 /* WebDAV specific */
637
638 - (EOQualifier *)parseSQLWhereExpression:(NSString *)_sql {
639   // TODO: process %=>* and %%, and $
640   static EOSQLQualifierIdMapper *map = nil;
641   EOQualifier *q;
642
643   if ((q = [self _parseSQLWhereExpression:_sql]) == nil)
644     return nil;
645   
646   if (map == nil) map = [[EOSQLQualifierIdMapper alloc] init];
647   q = [q qualifierByApplyingKeyMap:(id)map];
648   return q;
649 }
650
651 - (BOOL)parseColumnName:(NSString **)result
652   from:(unichar **)pos length:(unsigned *)len
653   consume:(BOOL)consume
654 {
655   BOOL ok;
656   
657   ok = [self parseIdentifier:result from:pos length:len consume:consume];
658   if (!ok) return NO;
659   
660   if (*result) {
661     /* heuristics to morph fully qualified name to namespace/localname */
662     NSRange r;
663     NSString *l, *ns;
664     
665     r = [*result rangeOfString:@"/" 
666                  options:NSLiteralSearch|NSBackwardsSearch];
667     if (r.length == 0) {
668       r = [*result rangeOfString:@":" 
669                    options:NSLiteralSearch|NSBackwardsSearch];
670     }
671     if (r.length == 0)
672       return YES;
673     
674     l  = [*result substringFromIndex:(r.location + r.length)];
675     ns = [*result substringToIndex:(r.location + 1)];
676     
677     [*result autorelease];
678     *result = [[NSString alloc] initWithFormat:@"{%@}%@", ns, l];
679   }
680   return YES;
681 }
682
683 @end /* SoDAVSQLParser */
684
685 @implementation SoDAVSQLParser(Tests)
686
687 + (void)testDAVQuery {
688   EOFetchSpecification *fs;
689   NSString *sql;
690   
691   NSLog(@"testing: %@ --------------------", self);
692
693   sql = @"\n"
694   @"select \n"
695   @"  \"http://schemas.microsoft.com/mapi/proptag/x0e230003\",        \n"
696   @"  \"urn:schemas:mailheader:subject\",        \n"
697   @"  \"urn:schemas:mailheader:from\",\n"
698   @"  \"urn:schemas:mailheader:to\",        \n"
699   @"  \"urn:schemas:mailheader:cc\",        \n"
700   @"  \"urn:schemas:httpmail:read\",        \n"
701   @"  \"urn:schemas:httpmail:hasattachment\",        \n"
702   @"  \"DAV:getcontentlength\",        \n"
703   @"  \"urn:schemas:mailheader:date\",        \n"
704   @"  \"urn:schemas:httpmail:date\",      \n"
705   @"  \"urn:schemas:mailheader:received\",        \n"
706   @"  \"urn:schemas:mailheader:message-id\",        \n"
707   @"  \"urn:schemas:mailheader:in-reply-to\",        \n"
708   @"  \"urn:schemas:mailheader:references\"      \n"
709   @"from \n"
710   @"  scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
711   @"where \n"
712   @"  \"DAV:iscollection\" = False \n"
713   @"  and \n"
714   @"  \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
715   @"  and \n"
716   @"  \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0  \n"
717   @"  \n";
718   fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
719   
720   NSLog(@"  FS: %@", fs);
721   if (fs == nil) {
722     [self errorWithFormat:@"  could not parse SQL: %@", sql];
723   }
724   else {
725     EOQualifier *q;
726     NSString *scope;
727     NSArray  *props;
728     
729     if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
730       NSLog(@"  INVALID: got no scope !");
731     if (![scope isEqualToString:@"flat"])
732       NSLog(@"  INVALID: got scope %@, expected flat !", scope);
733
734 #if 0    
735     if ([fs queryWebDAVPropertyNamesOnly])
736       NSLog(@"  INVALID: name query only, but queried several attrs !");
737 #endif
738     
739     /* check qualifier */
740     if ((q = [fs qualifier]) == nil)
741       NSLog(@"  INVALID: got not qualifier (expected one) !");
742     else if (![q isKindOfClass:[EOAndQualifier class]]) {
743       NSLog(@"  INVALID: expected AND qualifier, got %@ !",
744             NSStringFromClass([q class]));
745     }
746     else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
747       NSLog(@"  INVALID: expected 3 subqualifiers, got %i !",
748             [[(EOAndQualifier *)q qualifiers] count]);
749     }
750
751     /* check sortordering */
752     if ([fs sortOrderings] != nil) {
753       NSLog(@"  INVALID: got sort orderings, specified none: %@ !",
754             [fs sortOrderings]);
755     }
756     
757     /* attributes */
758     if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
759       NSLog(@"  INVALID: got not attributes (expected some) !");
760     else if (![props isKindOfClass:[NSArray class]]) {
761       NSLog(@"  INVALID: attributes not delivered as array ?: %@",
762             NSStringFromClass([props class]));
763     }
764     else if ([props count] != 14) {
765       NSLog(@"  INVALID: invalid attribute count, expected 14, got %i.",
766             [props count]);
767     }
768   }
769   
770   NSLog(@"done test: %@ ------------------", self);
771 }
772
773 @end /* SoDAVSQLParser(Tests) */
774
775 @implementation EOSQLQualifierIdMapper
776
777 - (id)objectForKey:(NSString *)_key {
778   NSRange  r;
779   NSString *l, *ns;
780   
781   if (_key == nil) return nil;
782   
783   r = [_key rangeOfString:@"/" options:NSLiteralSearch|NSBackwardsSearch];
784   if (r.length == 0)
785     r = [_key rangeOfString:@":" options:NSLiteralSearch|NSBackwardsSearch];
786   if (r.length == 0)
787     return _key;
788   
789   l  = [_key substringFromIndex:(r.location + r.length)];
790   ns = [_key substringToIndex:(r.location + 1)];
791   
792   return [NSString stringWithFormat:@"{%@}%@", ns, l];
793 }
794
795 @end /* EOSQLQualifierIdMapper */