2 Copyright (C) 2000-2003 SKYRIX Software AG
4 This file is part of OGo
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
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.
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
23 #include "EOSQLParser.h"
24 #include "EOQualifier.h"
25 #include "EOFetchSpecification.h"
26 #include "EOSortOrdering.h"
27 #include "EOClassDescription.h"
30 // TODO: better error output
32 @interface EOSQLParser(Logging) /* this is available in NGExtensions */
33 - (void)logWithFormat:(NSString *)_fmt,...;
36 @implementation EOSQLParser
38 + (id)sharedSQLParser {
39 static EOSQLParser *sharedParser = nil; // THREAD
40 if (sharedParser == nil)
41 sharedParser = [[EOSQLParser alloc] init];
49 /* top level parsers */
51 - (EOFetchSpecification *)parseSQLSelectStatement:(NSString *)_sql {
52 EOFetchSpecification *fs;
54 unsigned len, remainingLen;
56 if ((len = [_sql length]) == 0) return nil;
58 us = calloc(len + 10, sizeof(unichar));
59 [_sql getCharacters:us];
64 if (![self parseSQL:&fs from:&pos length:&remainingLen strict:NO])
65 [self logWithFormat:@"parsing of SQL failed."];
69 return [fs autorelease];
72 - (EOQualifier *)parseSQLWhereExpression:(NSString *)_sql {
73 // TODO: process %=>* and %%, and $
77 if ((len = [_sql length]) == 0) return nil;
79 // TODO: improve, real parsing in qualifier parser !
81 buf = calloc(len + 3, sizeof(unichar));
82 NSAssert(buf, @"could not allocate char buffer");
84 [_sql getCharacters:buf];
85 for (i = 0, didReplace = NO; i < len; i++) {
88 NSLog(@"WARNING(%s): SQL string contains a '*': %@",
89 __PRETTY_FUNCTION__, _sql);
97 _sql = [NSString stringWithCharacters:buf length:len];
100 return [EOQualifier qualifierWithQualifierFormat:_sql];
103 /* parsing parts (exported for overloading in subclasses) */
106 uniIsCEq(unichar *haystack, const unsigned char *needle, unsigned len)
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;
115 static inline void skipSpaces(unichar **pos, unsigned *len) {
117 if (!isspace(*pos[0])) return;
122 static void printUniStr(unichar *pos, unsigned len) __attribute__((unused));
123 static void printUniStr(unichar *pos, unsigned len) {
125 for (i = 0; i < len && i < 80; i++)
130 static inline BOOL isTokStopChar(unichar c) {
133 case ')': case '(': case '"': case '\'':
136 if (isspace(c)) return YES;
141 - (BOOL)parseToken:(const unsigned char *)tk
142 from:(unichar **)pos length:(unsigned *)len
143 consume:(BOOL)consume
145 /* ...[space] (strlen(tk)+1 chars) */
150 scur=*pos; slen=*len; // begin transaction
151 skipSpaces(&scur, &slen);
155 if (toupper(scur[0]) != tk[0])
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 */
161 if (!uniIsCEq(scur, tk, tlen))
164 scur+=tlen; slen-=tlen;
166 if (consume) { *pos = scur; *len = slen; } // end tx
170 - (BOOL)parseIdentifier:(NSString **)result
171 from:(unichar **)pos length:(unsigned *)len
172 consume:(BOOL)consume
174 /* "attr" or attr (at least 1 char or 2 for ") */
178 if (result) *result = nil;
179 scur=*pos; slen=*len; // begin transaction
180 skipSpaces(&scur, &slen);
186 //printf("try quoted attr\n");
187 if (slen < 2) return NO;
188 scur++; slen--; /* skip quote */
192 if (consume) { *pos = scur; *len = slen; } // end transaction
194 //printf("is empty quoted\n");
197 if (slen < 2) return NO;
200 while ((slen > 0) && (*scur != '"')) {
201 if (*scur == '\\' && (slen > 1)) {
203 scur++; slen--; // skip one more (still needs to be filtered in result
207 if (slen > 0) { scur++; slen--; } /* skip quote */
209 // TODO: xhandle contained quoted chars ?
211 [[NSString alloc] initWithCharacters:start length:(scur-start-1)];
212 //NSLog(@"found qattr: %@", *result);
215 /* non-quoted attr */
218 if (slen < 1) return NO;
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");
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");
232 while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
236 *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
237 //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
239 if (consume && result) { *pos = scur; *len = slen; } // end transaction
240 return *result ? YES : NO;
242 - (BOOL)parseColumnName:(NSString **)result
243 from:(unichar **)pos length:(unsigned *)len
244 consume:(BOOL)consume
246 return [self parseIdentifier:result from:pos length:len consume:consume];
248 - (BOOL)parseTableName:(NSString **)result
249 from:(unichar **)pos length:(unsigned *)len
250 consume:(BOOL)consume
252 return [self parseIdentifier:result from:pos length:len consume:consume];
255 - (BOOL)parseIdentifierList:(NSArray **)result
256 from:(unichar **)pos length:(unsigned *)len
260 NSMutableArray *attrs = nil;
264 BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
266 if (result) *result = nil;
267 scur=*pos; slen=*len; // begin transaction
268 skipSpaces(&scur, &slen);
269 parser = (void *)[self methodForSelector:_sel];
271 if (slen < 1) return NO; // not enough chars
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 */
281 *pos = scur; *len = slen; // end transaction
286 if (!parser(self, _sel, &attr,&scur,&slen,YES))
287 /* well, we need at least one attribute to make it a list */
290 attrs = [[NSMutableArray alloc] initWithCapacity:32];
291 [attrs addObject:attr]; [attr release];
293 /* all the remaining attributes must be prefixed with a "," */
295 //printf("try next list attr comma\n");
296 skipSpaces(&scur, &slen);
298 if (*scur != ',') break;
299 scur++; slen--; // skip ','
301 //printf("try next list attr\n");
302 if (!parser(self, _sel, &attr,&scur,&slen,YES))
305 [attrs addObject:attr]; [attr release];
308 *pos = scur; *len = slen; // end transaction
313 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
314 from:(unichar **)pos length:(unsigned *)len
316 /* contains('"hh@"') [12+ chars] */
321 skipSpaces(&scur, &slen);
323 if (slen < 12) return NO; // not enough chars
325 if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
327 skipSpaces(&scur, &slen);
328 [self parseToken:"('" from:&scur length:&slen consume:YES];
330 if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
333 skipSpaces(&scur, &slen);
334 [self parseToken:"')" from:&scur length:&slen consume:YES];
336 *q_ = [[EOQualifier qualifierWithQualifierFormat:
337 @"contentAsString doesContain: %@", s] retain];
339 *pos = scur; *len = slen; // end transaction
346 - (BOOL)parseQualifier:(EOQualifier **)result
347 from:(unichar **)pos length:(unsigned *)len
352 if (result) *result = nil;
353 scur=*pos; slen=*len; // begin transaction
354 skipSpaces(&scur, &slen);
356 if (slen < 3) return NO; // not enough chars
358 // for now should scan till we find either ORDER BY order GROUP BY
360 unichar *start = scur;
363 if (*scur == 'O' || *scur == 'o') {
364 if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
365 //printf("FOUND ORDER TOKEN ...\n");
369 else if (*scur == 'G' || *scur == 'g') {
370 if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
371 //printf("FOUND GROUP TOKEN ...\n");
383 s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
384 if ([s length] == 0) {
388 if ((q = [self parseSQLWhereExpression:s]) == nil) {
392 *result = [q retain];
397 *pos = scur; *len = slen; // end transaction
401 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
402 from:(unichar **)pos length:(unsigned *)len
405 "('shallow traversal of "..."')"
406 "('hierarchical traversal of "..."')"
410 NSString *entityName;
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
420 if (*scur != '(') return NO; // does not start with '('
421 scur++; slen--; // skip '('
422 skipSpaces(&scur, &slen);
424 if (*scur != '\'') return NO; // does not start with '(''
425 scur++; slen--; // skip single quote
429 if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
431 else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
433 else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
436 /* unknown traveral key */
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
445 skipSpaces(&scur, &slen);
446 if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
447 return NO; // failed to parse entity from scope
450 skipSpaces(&scur, &slen);
451 if (slen > 0 && *scur == '\'') {
452 scur++; slen--; // skip single quote
454 skipSpaces(&scur, &slen);
455 if (slen > 0 && *scur == ')') {
456 scur++; slen--; // skip ')'
459 if (_scope) *_scope = isShallow ? @"flat" : @"deep";
460 if (_entity) *_entity = entityName;
461 *pos = scur; *len = slen; // end transaction
465 - (BOOL)parseSELECT:(EOFetchSpecification **)result
466 from:(unichar **)pos length:(unsigned *)len
467 strict:(BOOL)beStrict
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;
479 BOOL missingByOfOrder = NO;
480 BOOL missingByOfGroup = NO;
484 if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
485 /* must begin with SELECT */
486 if (beStrict) return NO;
491 if (![self parseIdentifierList:&attrs from:pos length:len
492 selector:@selector(parseColumnName:from:length:consume:)]) {
493 [self logWithFormat:@"missing ID list .."];
496 //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
498 /* now a from is expected */
499 if ([self parseToken:"FROM" from:pos length:len consume:YES])
502 if (beStrict) return NO;
505 /* check whether it's followed by a scope */
506 if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
507 NSString *scopeEntity = nil;
509 if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
510 if (beStrict) return NO;
514 [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
518 fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
519 [scopeEntity release];
522 if (![self parseIdentifierList:&fromList from:pos length:len
523 selector:@selector(parseTableName:from:length:consume:)]) {
524 [self logWithFormat:@"missing from list .."];
528 [self logWithFormat:@"parsed FROM list (%i): %@",
529 [fromList count], fromList];
534 if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
535 /* parse qualifier ... */
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;
542 else if (![self parseQualifier:&q from:pos length:len]) {
543 if (beStrict) return NO;
546 [self logWithFormat:@"FOUND Qualifier: '%@'", q];
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;
557 if (![self parseIdentifierList:&orderList from:pos length:len
558 selector:@selector(parseColumnName:from:length:consume:)])
561 [self logWithFormat:@"parsed ORDER list (%i): %@",
562 [orderList count], orderList];
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;
574 //printUniStr(*pos, *len); // DEBUG
576 if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
577 if (!hasFrom) [self logWithFormat:@"missing FROM !"];
578 if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
580 /* build fetchspec */
582 lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
585 [lHints setObject:scope forKey:@"scope"];
586 [scope release]; scope = nil;
589 [lHints setObject:attrs forKey:@"attributes"];
590 [attrs release]; attrs = nil;
596 len = [orderList count];
597 ma = [[NSMutableArray alloc] initWithCapacity:len];
598 for (i = 0; i < len; i++) {
601 so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
602 selector:EOCompareAscending];
604 lSortOrderings = [ma shallowCopy];
606 [orderList release]; orderList = nil;
609 fs = [[EOFetchSpecification alloc]
610 initWithEntityName:[fromList componentsJoinedByString:@","]
612 sortOrderings:lSortOrderings
613 usesDistinct:NO isDeep:NO hints:lHints];
619 return fs ? YES : NO;
622 - (BOOL)parseSQL:(id *)result
623 from:(unichar **)pos length:(unsigned *)len
624 strict:(BOOL)beStrict
626 if (*len < 1) return NO;
628 if ([self parseToken:"SELECT" from:pos length:len consume:NO])
629 return [self parseSELECT:result from:pos length:len strict:beStrict];
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])
635 [self logWithFormat:@"tried to parse an unsupported SQL statement."];
639 @end /* EOSQLParser */
641 @implementation EOSQLParser(Tests)
643 + (void)testDAVQuery {
644 EOFetchSpecification *fs;
647 NSLog(@"testing: %@ --------------------", self);
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"
666 @" scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
668 @" \"DAV:iscollection\" = False \n"
670 @" \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
672 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0 \n"
674 fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
676 NSLog(@" FS: %@", fs);
678 NSLog(@" ERROR: could not parse SQL: %@", sql);
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);
691 if ([fs queryWebDAVPropertyNamesOnly])
692 NSLog(@" INVALID: name query only, but queried several attrs !");
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]));
702 else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
703 NSLog(@" INVALID: expected 3 subqualifiers, got %i !",
704 [[(EOAndQualifier *)q qualifiers] count]);
707 /* check sortordering */
708 if ([fs sortOrderings] != nil) {
709 NSLog(@" INVALID: got sort orderings, specified none: %@ !",
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]));
720 else if ([props count] != 14) {
721 NSLog(@" INVALID: invalid attribute count, expected 14, got %i.",
726 NSLog(@"done test: %@ ------------------", self);
729 @end /* EOSQLParser(Tests) */