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