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