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