]> err.no Git - sope/blob - sope-core/EOControl/EOSQLParser.m
fixed copyrights for 2005
[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(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:"FROM" from:&scur length:&slen consume:NO]) {
220       /* not an attribute, the from starts ... */
221       // printf("rejected unquoted attr, is a FROM\n");
222       return NO;
223     }
224     if ([self parseToken:"WHERE" from:&scur length:&slen consume:NO]) {
225       /* not an attribute, the where starts ... */
226       // printf("rejected unquoted attr, is a WHERE\n");
227       return NO;
228     }
229     
230     start = scur;
231     while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
232       slen--;
233       scur++;
234     }
235     *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
236     //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
237   }
238   if (consume && result) { *pos = scur; *len = slen; } // end transaction
239   return *result ? YES : NO;
240 }
241 - (BOOL)parseColumnName:(NSString **)result
242   from:(unichar **)pos length:(unsigned *)len
243   consume:(BOOL)consume
244 {
245   return [self parseIdentifier:result from:pos length:len consume:consume];
246 }
247 - (BOOL)parseTableName:(NSString **)result
248   from:(unichar **)pos length:(unsigned *)len
249   consume:(BOOL)consume
250 {
251   return [self parseIdentifier:result from:pos length:len consume:consume];
252 }
253
254 - (BOOL)parseIdentifierList:(NSArray **)result
255   from:(unichar **)pos length:(unsigned *)len
256   selector:(SEL)_sel
257 {
258   /* attr[,attr] */
259   NSMutableArray *attrs = nil;
260   unichar  *scur;
261   unsigned slen;
262   id       attr;
263   BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
264   
265   if (result) *result = nil;
266   scur=*pos; slen=*len; // begin transaction
267   skipSpaces(&scur, &slen);
268   parser = (void *)[self methodForSelector:_sel];
269   
270   if (slen < 1) return NO; // not enough chars
271   
272   if (*scur == '*') {
273     /* a wildcard list, return 'nil' as result */
274     //printf("try wildcard\n");
275     scur++; slen--; // skip '*'
276     if (!(slen == 0 || isspace(*scur))) {
277       /* not followed by space or at end */
278       return NO;
279     }
280     *pos = scur; *len = slen; // end transaction
281     *result = nil;
282     return YES;
283   }
284   
285   if (!parser(self, _sel, &attr,&scur,&slen,YES))
286     /* well, we need at least one attribute to make it a list */
287     return NO;
288   
289   attrs = [[NSMutableArray alloc] initWithCapacity:32];
290   [attrs addObject:attr]; [attr release];
291   
292   /* all the remaining attributes must be prefixed with a "," */
293   while (slen > 1) {
294     //printf("try next list attr comma\n");
295     skipSpaces(&scur, &slen);
296     if (slen < 2) break;
297     if (*scur != ',') break;
298     scur++; slen--; // skip ','
299     
300     //printf("try next list attr\n");
301     if (!parser(self, _sel, &attr,&scur,&slen,YES))
302       break;
303     
304     [attrs addObject:attr]; [attr release];
305   }
306   
307   *pos = scur; *len = slen; // end transaction
308   *result = attrs;
309   return YES;
310 }
311
312 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
313   from:(unichar **)pos length:(unsigned *)len
314 {
315   /* contains('"hh@"') [12+ chars] */
316   unichar  *scur;
317   unsigned slen;
318   NSString *s;
319   if (q_) *q_ = nil;
320   skipSpaces(&scur, &slen);
321   
322   if (slen < 12) return NO; // not enough chars
323   
324   if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
325     return NO;
326   skipSpaces(&scur, &slen);
327   [self parseToken:"('" from:&scur length:&slen consume:YES];
328   
329   if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
330     return NO;
331   
332   skipSpaces(&scur, &slen);
333   [self parseToken:"')" from:&scur length:&slen consume:YES];
334   
335   *q_ = [[EOQualifier qualifierWithQualifierFormat:
336                         @"contentAsString doesContain: %@", s] retain];
337   if (*q_) {
338     *pos = scur; *len = slen; // end transaction
339     return YES;
340   }
341   else
342     return NO;
343 }
344
345 - (BOOL)parseQualifier:(EOQualifier **)result
346   from:(unichar **)pos length:(unsigned *)len
347 {
348   unichar  *scur;
349   unsigned slen;
350   
351   if (result) *result = nil;
352   scur=*pos; slen=*len; // begin transaction
353   skipSpaces(&scur, &slen);
354   
355   if (slen < 3) return NO; // not enough chars
356   
357   // for now should scan till we find either ORDER BY order GROUP BY
358   {
359     unichar *start = scur;
360     
361     while (slen > 0) {
362       if (*scur == 'O' || *scur == 'o') {
363         if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
364           //printf("FOUND ORDER TOKEN ...\n");
365           break;
366         }
367       }
368       else if (*scur == 'G' || *scur == 'g') {
369         if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
370           //printf("FOUND GROUP TOKEN ...\n");
371           break;
372         }
373       }
374       
375       scur++; slen--;
376     }
377
378     {
379       EOQualifier *q;
380       NSString *s;
381       
382       s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
383       if ([s length] == 0) {
384         [s release];
385         return NO;
386       }
387       if ((q = [self parseSQLWhereExpression:s]) == nil) {
388         [s release];
389         return NO;
390       }
391       *result = [q retain];
392       [s release];
393     }
394   }
395   
396   *pos = scur; *len = slen; // end transaction
397   return YES;
398 }
399
400 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
401   from:(unichar **)pos length:(unsigned *)len
402 {
403   /* 
404     "('shallow traversal of "..."')"
405     "('hierarchical traversal of "..."')"
406   */
407   unichar  *scur;
408   unsigned slen;
409   NSString *entityName;
410   BOOL isShallow = NO;
411   BOOL isDeep    = NO;
412   
413   if (_scope)  *_scope  = nil;
414   if (_entity) *_entity = nil;
415   scur=*pos; slen=*len; // begin transaction
416   skipSpaces(&scur, &slen);
417   if (slen < 14) return NO; // not enough chars
418   
419   if (*scur != '(') return NO; // does not start with '('
420   scur++; slen--; // skip '('
421   skipSpaces(&scur, &slen);
422   
423   if (*scur != '\'') return NO; // does not start with '(''
424   scur++; slen--; // skip single quote
425   
426   /* next the depth */
427   
428   if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
429     isShallow = YES;
430   else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
431     isDeep = YES;
432   else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
433     isDeep = YES;
434   else
435     /* unknown traveral key */
436     return NO;
437   
438   /* some syntactic sugar (not strict about that ...) */
439   [self parseToken:"TRAVERSAL" from:&scur length:&slen consume:YES];
440   [self parseToken:"OF"        from:&scur length:&slen consume:YES];
441   if (slen < 1) return NO; // not enough chars
442   
443   /* now the entity */
444   skipSpaces(&scur, &slen);
445   if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
446     return NO; // failed to parse entity from scope
447
448   /* trailer */
449   skipSpaces(&scur, &slen);
450   if (slen > 0 && *scur == '\'') {
451     scur++; slen--; // skip single quote
452   }
453   skipSpaces(&scur, &slen);
454   if (slen > 0 && *scur == ')') {
455     scur++; slen--; // skip ')'
456   }
457   
458   if (_scope)  *_scope  = isShallow ? @"flat" : @"deep";
459   if (_entity) *_entity = entityName;
460   *pos = scur; *len = slen; // end transaction
461   return YES;
462 }
463
464 - (BOOL)parseSELECT:(EOFetchSpecification **)result
465   from:(unichar **)pos length:(unsigned *)len
466   strict:(BOOL)beStrict
467 {
468   EOFetchSpecification *fs;
469   NSMutableDictionary *lHints;
470   NSString *scope     = nil;
471   NSArray  *attrs     = nil;
472   NSArray  *fromList  = nil;
473   NSArray  *orderList = nil;
474   NSArray  *lSortOrderings = nil;
475   EOQualifier *q = nil;
476   BOOL hasSelect = NO;
477   BOOL hasFrom   = NO;
478   BOOL missingByOfOrder = NO;
479   BOOL missingByOfGroup = NO;
480   
481   *result = nil;
482   
483   if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
484     /* must begin with SELECT */
485     if (beStrict) return NO;
486   }
487   else
488     hasSelect = YES;
489   
490   if (![self parseIdentifierList:&attrs from:pos length:len
491              selector:@selector(parseColumnName:from:length:consume:)]) {
492     [self logWithFormat:@"missing ID list .."];
493     return NO;
494   }
495   //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
496   
497   /* now a from is expected */
498   if ([self parseToken:"FROM" from:pos length:len consume:YES])
499     hasFrom = YES;
500   else {
501     if (beStrict) return NO;
502   }
503   
504   /* check whether it's followed by a scope */
505   if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
506     NSString *scopeEntity = nil;
507     
508     if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
509       if (beStrict) return NO;
510     }
511 #if DEBUG_PARSING
512     else
513       [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
514 #endif
515     
516     if (scopeEntity)
517       fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
518     [scopeEntity release];
519   }
520   else {
521     if (![self parseIdentifierList:&fromList from:pos length:len
522                selector:@selector(parseTableName:from:length:consume:)]) {
523       [self logWithFormat:@"missing from list .."];
524       return NO;
525     }
526 #if DEBUG_PARSING
527     [self logWithFormat:@"parsed FROM list (%i): %@",
528           [fromList count], fromList];
529 #endif
530   }
531   
532   /* check where */
533   if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
534     /* parse qualifier ... */
535     
536     if ([self parseToken:"CONTAINS" from:pos length:len consume:NO]) {
537       if (![self parseContainsQualifier:&q from:pos length:len]) {
538         if (beStrict) return NO;
539       }
540     }
541     else if (![self parseQualifier:&q from:pos length:len]) {
542       if (beStrict) return NO;
543     }
544 #if DEBUG_PARSING
545     [self logWithFormat:@"FOUND Qualifier: '%@'", q];
546 #endif
547   }
548   
549   /* check order-by */
550   if ([self parseToken:"ORDER" from:pos length:len consume:YES]) {
551     if (![self parseToken:"BY" from:pos length:len consume:YES]) {
552       if (beStrict) return NO;
553       missingByOfOrder = YES;
554     }
555     
556     if (![self parseIdentifierList:&orderList from:pos length:len
557                selector:@selector(parseColumnName:from:length:consume:)])
558       return NO;
559 #if DEBUG_PARSING
560     [self logWithFormat:@"parsed ORDER list (%i): %@", 
561             [orderList count], orderList];
562 #endif
563   }
564   
565   /* check group-by */
566   if ([self parseToken:"GROUP" from:pos length:len consume:YES]) {
567     if (![self parseToken:"BY" from:pos length:len consume:YES]) {
568       if (beStrict) return NO;
569       missingByOfGroup = YES;
570     }
571   }
572   
573   //printUniStr(*pos, *len); // DEBUG
574   
575   if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
576   if (!hasFrom)   [self logWithFormat:@"missing FROM !"];
577   if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
578
579   /* build fetchspec */
580
581   lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
582   
583   if (scope) {
584     [lHints setObject:scope forKey:@"scope"];
585     [scope release]; scope = nil;
586   }
587   if (attrs) {
588     [lHints setObject:attrs forKey:@"attributes"];
589     [attrs release]; attrs = nil;
590   }
591   if (orderList) {
592     NSMutableArray *ma;
593     unsigned i, len;
594     
595     len = [orderList count];
596     ma = [[NSMutableArray alloc] initWithCapacity:len];
597     for (i = 0; i < len; i++) {
598       EOSortOrdering *so;
599       
600       so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
601                            selector:EOCompareAscending];
602     }
603     lSortOrderings = [ma shallowCopy];
604     [ma release];
605     [orderList release]; orderList = nil;
606   }
607   
608   fs = [[EOFetchSpecification alloc]
609          initWithEntityName:[fromList componentsJoinedByString:@","]
610          qualifier:q
611          sortOrderings:lSortOrderings
612          usesDistinct:NO isDeep:NO hints:lHints];
613   [lHints release];
614   [q release];
615   [fromList release];
616   
617   *result = fs;
618   return fs ? YES : NO;
619 }
620
621 - (BOOL)parseSQL:(id *)result
622   from:(unichar **)pos length:(unsigned *)len
623   strict:(BOOL)beStrict
624 {
625   if (*len < 1) return NO;
626   
627   if ([self parseToken:"SELECT" from:pos length:len consume:NO])
628     return [self parseSELECT:result from:pos length:len strict:beStrict];
629   
630   //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
631   //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
632   //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
633   
634   [self logWithFormat:@"tried to parse an unsupported SQL statement."];
635   return NO;
636 }
637
638 @end /* EOSQLParser */
639
640 @implementation EOSQLParser(Tests)
641
642 + (void)testDAVQuery {
643   EOFetchSpecification *fs;
644   NSString *sql;
645   
646   NSLog(@"testing: %@ --------------------", self);
647
648   sql = @"\n"
649   @"select \n"
650   @"  \"http://schemas.microsoft.com/mapi/proptag/x0e230003\",        \n"
651   @"  \"urn:schemas:mailheader:subject\",        \n"
652   @"  \"urn:schemas:mailheader:from\",\n"
653   @"  \"urn:schemas:mailheader:to\",        \n"
654   @"  \"urn:schemas:mailheader:cc\",        \n"
655   @"  \"urn:schemas:httpmail:read\",        \n"
656   @"  \"urn:schemas:httpmail:hasattachment\",        \n"
657   @"  \"DAV:getcontentlength\",        \n"
658   @"  \"urn:schemas:mailheader:date\",        \n"
659   @"  \"urn:schemas:httpmail:date\",      \n"
660   @"  \"urn:schemas:mailheader:received\",        \n"
661   @"  \"urn:schemas:mailheader:message-id\",        \n"
662   @"  \"urn:schemas:mailheader:in-reply-to\",        \n"
663   @"  \"urn:schemas:mailheader:references\"      \n"
664   @"from \n"
665   @"  scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
666   @"where \n"
667   @"  \"DAV:iscollection\" = False \n"
668   @"  and \n"
669   @"  \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
670   @"  and \n"
671   @"  \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0  \n"
672   @"  \n";
673   fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
674   
675   NSLog(@"  FS: %@", fs);
676   if (fs == nil) {
677     NSLog(@"  ERROR: could not parse SQL: %@", sql);
678   }
679   else {
680     EOQualifier *q;
681     NSString *scope;
682     NSArray  *props;
683     
684     if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
685       NSLog(@"  INVALID: got no scope !");
686     if (![scope isEqualToString:@"flat"])
687       NSLog(@"  INVALID: got scope %@, expected flat !", scope);
688
689 #if 0    
690     if ([fs queryWebDAVPropertyNamesOnly])
691       NSLog(@"  INVALID: name query only, but queried several attrs !");
692 #endif
693     
694     /* check qualifier */
695     if ((q = [fs qualifier]) == nil)
696       NSLog(@"  INVALID: got not qualifier (expected one) !");
697     else if (![q isKindOfClass:[EOAndQualifier class]]) {
698       NSLog(@"  INVALID: expected AND qualifier, got %@ !",
699             NSStringFromClass([q class]));
700     }
701     else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
702       NSLog(@"  INVALID: expected 3 subqualifiers, got %i !",
703             [[(EOAndQualifier *)q qualifiers] count]);
704     }
705
706     /* check sortordering */
707     if ([fs sortOrderings] != nil) {
708       NSLog(@"  INVALID: got sort orderings, specified none: %@ !",
709             [fs sortOrderings]);
710     }
711     
712     /* attributes */
713     if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
714       NSLog(@"  INVALID: got not attributes (expected some) !");
715     else if (![props isKindOfClass:[NSArray class]]) {
716       NSLog(@"  INVALID: attributes not delivered as array ?: %@",
717             NSStringFromClass([props class]));
718     }
719     else if ([props count] != 14) {
720       NSLog(@"  INVALID: invalid attribute count, expected 14, got %i.",
721             [props count]);
722     }
723   }
724   
725   NSLog(@"done test: %@ ------------------", self);
726 }
727
728 @end /* EOSQLParser(Tests) */