parser_p.cpp
1/* This file is part of the KDE project
2 Copyright (C) 2004, 2006 Jaroslaw Staniek <js@iidea.pl>
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18*/
19
20#include "parser_p.h"
21#include "sqlparser.h"
22
23#include <kdebug.h>
24#include <tdelocale.h>
25
26#include <tqregexp.h>
27
28#include <assert.h>
29
30using namespace KexiDB;
31
32Parser *parser = 0;
33Field *field = 0;
34//bool requiresTable;
35TQPtrList<Field> fieldList;
36int current = 0;
37TQString ctoken = "";
38
39//-------------------------------------
40
41ParserPrivate::ParserPrivate()
42 : reservedKeywords(997, 997, false)
43 , initialized(false)
44{
45 clear();
46 table = 0;
47 select = 0;
48 db = 0;
49}
50
51ParserPrivate::~ParserPrivate()
52{
53 delete select;
54 delete table;
55}
56
57void ParserPrivate::clear()
58{
59 operation = Parser::OP_None;
60 error = ParserError();
61}
62
63//-------------------------------------
64
65ParseInfo::ParseInfo(KexiDB::QuerySchema *query)
66 : repeatedTablesAndAliases(997, false)
67 , querySchema(query)
68{
69 repeatedTablesAndAliases.setAutoDelete(true);
70}
71
72ParseInfo::~ParseInfo()
73{
74}
75
76//-------------------------------------
77
78extern int yyparse();
79extern void tokenize(const char *data);
80
81void yyerror(const char *str)
82{
83 KexiDBDbg << "error: " << str << endl;
84 KexiDBDbg << "at character " << current << " near tooken " << ctoken << endl;
85 parser->setOperation(Parser::OP_Error);
86
87 const bool otherError = (tqstrnicmp(str, "other error", 11)==0);
88
89 if (parser->error().type().isEmpty()
90 && (str==0 || strlen(str)==0
91 || tqstrnicmp(str, "syntax error", 12)==0 || tqstrnicmp(str, "parse error", 11)==0)
92 || otherError)
93 {
94 KexiDBDbg << parser->statement() << endl;
95 TQString ptrline = "";
96 for(int i=0; i < current; i++)
97 ptrline += " ";
98
99 ptrline += "^";
100
101 KexiDBDbg << ptrline << endl;
102
103 //lexer may add error messages
104 TQString lexerErr = parser->error().error();
105
106 TQString errtypestr(str);
107 if (lexerErr.isEmpty()) {
108#if 0
109 if (errtypestr.startsWith("parse error, unexpected ")) {
110 //something like "parse error, unexpected IDENTIFIER, expecting ',' or ')'"
111 TQString e = errtypestr.mid(24);
112 KexiDBDbg << e <<endl;
113 TQString token = "IDENTIFIER";
114 if (e.startsWith(token)) {
115 TQRegExp re("'.'");
116 int pos=0;
117 pos = re.search(e, pos);
118 TQStringList captured=re.capturedTexts();
119 if (captured.count()>=2) {
120// KexiDBDbg << "**" << captured.at(1) << endl;
121// KexiDBDbg << "**" << captured.at(2) << endl;
122 }
123 }
124
125
126
127// IDENTIFIER, expecting '")) {
128 e = errtypestr.mid(47);
129 KexiDBDbg << e <<endl;
130// ,' or ')'
131// lexerErr i18n("identifier was expected");
132
133 } else
134#endif
135 if (errtypestr.startsWith("parse error, expecting `IDENTIFIER'"))
136 lexerErr = i18n("identifier was expected");
137 }
138
139 if (!otherError) {
140 if (!lexerErr.isEmpty())
141 lexerErr.prepend(": ");
142
143 if (parser->isReservedKeyword(ctoken.latin1()))
144 parser->setError( ParserError(i18n("Syntax Error"),
145 i18n("\"%1\" is a reserved keyword").arg(ctoken)+lexerErr, ctoken, current) );
146 else
147 parser->setError( ParserError(i18n("Syntax Error"),
148 i18n("Syntax Error near \"%1\"").arg(ctoken)+lexerErr, ctoken, current) );
149 }
150 }
151}
152
153void setError(const TQString& errName, const TQString& errDesc)
154{
155 parser->setError( ParserError(errName, errDesc, ctoken, current) );
156 yyerror(errName.latin1());
157}
158
159void setError(const TQString& errDesc)
160{
161 setError("other error", errDesc);
162}
163
164/* this is better than assert() */
165#define IMPL_ERROR(errmsg) setError("Implementation error", errmsg)
166
167bool parseData(Parser *p, const char *data)
168{
169/* todo: make this REENTRANT */
170 parser = p;
171 parser->clear();
172 field = 0;
173 fieldList.clear();
174// requiresTable = false;
175
176 if (!data) {
177 ParserError err(i18n("Error"), i18n("No query specified"), ctoken, current);
178 parser->setError(err);
179 yyerror("");
180 parser = 0;
181 return false;
182 }
183
184 tokenize(data);
185 if (!parser->error().type().isEmpty()) {
186 parser = 0;
187 return false;
188 }
189 yyparse();
190
191 bool ok = true;
192 if(parser->operation() == Parser::OP_Select)
193 {
194 KexiDBDbg << "parseData(): ok" << endl;
195// KexiDBDbg << "parseData(): " << tableDict.count() << " loaded tables" << endl;
196/* TableSchema *ts;
197 for(TQDictIterator<TableSchema> it(tableDict); TableSchema *s = tableList.first(); s; s = tableList.next())
198 {
199 KexiDBDbg << " " << s->name() << endl;
200 }*/
201/*removed
202 Field::ListIterator it = parser->select()->fieldsIterator();
203 for(Field *item; (item = it.current()); ++it)
204 {
205 if(tableList.findRef(item->table()) == -1)
206 {
207 ParserError err(i18n("Field List Error"), i18n("Unknown table '%1' in field list").arg(item->table()->name()), ctoken, current);
208 parser->setError(err);
209
210 yyerror("fieldlisterror");
211 ok = false;
212 }
213 }*/
214 //take the dummy table out of the query
215// parser->select()->removeTable(dummy);
216 }
217 else {
218 ok = false;
219 }
220
221// tableDict.clear();
222 parser = 0;
223 return ok;
224}
225
226
227/* Adds \a column to \a querySchema. \a column can be in a form of
228 table.field, tableAlias.field or field
229*/
230bool addColumn( ParseInfo& parseInfo, BaseExpr* columnExpr )
231{
232 if (!columnExpr->validate(parseInfo)) {
233 setError(parseInfo.errMsg, parseInfo.errDescr);
234 return false;
235 }
236
237 VariableExpr *v_e = columnExpr->toVariable();
238 if (columnExpr->exprClass() == KexiDBExpr_Variable && v_e) {
239 //it's a variable:
240 if (v_e->name=="*") {//all tables asterisk
241 if (parseInfo.querySchema->tables()->isEmpty()) {
242 setError(i18n("\"*\" could not be used if no tables are specified"));
243 return false;
244 }
245 parseInfo.querySchema->addAsterisk( new QueryAsterisk(parseInfo.querySchema) );
246 }
247 else if (v_e->tableForQueryAsterisk) {//one-table asterisk
248 parseInfo.querySchema->addAsterisk(
249 new QueryAsterisk(parseInfo.querySchema, v_e->tableForQueryAsterisk) );
250 }
251 else if (v_e->field) {//"table.field" or "field" (bound to a table or not)
252 parseInfo.querySchema->addField(v_e->field, v_e->tablePositionForField);
253 }
254 else {
255 IMPL_ERROR("addColumn(): unknown case!");
256 return false;
257 }
258 return true;
259 }
260
261 //it's complex expression
262 parseInfo.querySchema->addExpression(columnExpr);
263
264#if 0
265 KexiDBDbg << "found variable name: " << varName << endl;
266 int dotPos = varName.find('.');
267 TQString tableName, fieldName;
268//TODO: shall we also support db name?
269 if (dotPos>0) {
270 tableName = varName.left(dotPos);
271 fieldName = varName.mid(dotPos+1);
272 }
273 if (tableName.isEmpty()) {//fieldname only
274 fieldName = varName;
275 if (fieldName=="*") {
276 parseInfo.querySchema->addAsterisk( new QueryAsterisk(parseInfo.querySchema) );
277 }
278 else {
279 //find first table that has this field
280 Field *firstField = 0;
281 for (TableSchema::ListIterator it(*parseInfo.querySchema->tables()); it.current(); ++it) {
282 Field *f = it.current()->field(fieldName);
283 if (f) {
284 if (!firstField) {
285 firstField = f;
286 } else if (f->table()!=firstField->table()) {
287 //ambiguous field name
288 setError(i18n("Ambiguous field name"),
289 i18n("Both table \"%1\" and \"%2\" have defined \"%3\" field. "
290 "Use \"<tableName>.%4\" notation to specify table name.")
291 .arg(firstField->table()->name()).arg(f->table()->name())
292 .arg(fieldName).arg(fieldName));
293 return false;
294 }
295 }
296 }
297 if (!firstField) {
298 setError(i18n("Field not found"),
299 i18n("Table containing \"%1\" field not found").arg(fieldName));
300 return false;
301 }
302 //ok
303 parseInfo.querySchema->addField(firstField);
304 }
305 }
306 else {//table.fieldname or tableAlias.fieldname
307 tableName = tableName.lower();
308 TableSchema *ts = parseInfo.querySchema->table( tableName );
309 if (ts) {//table.fieldname
310 //check if "table" is covered by an alias
311 const TQValueList<int> tPositions = parseInfo.querySchema->tablePositions(tableName);
312 TQValueList<int>::ConstIterator it = tPositions.constBegin();
313 TQCString tableAlias;
314 bool covered = true;
315 for (; it!=tPositions.constEnd() && covered; ++it) {
316 tableAlias = parseInfo.querySchema->tableAlias(*it);
317 if (tableAlias.isEmpty() || tableAlias.lower()==tableName.latin1())
318 covered = false; //uncovered
319 KexiDBDbg << " --" << "covered by " << tableAlias << " alias" << endl;
320 }
321 if (covered) {
322 setError(i18n("Could not access the table directly using its name"),
323 i18n("Table \"%1\" is covered by aliases. Instead of \"%2\", "
324 "you can write \"%3\"").arg(tableName)
325 .arg(tableName+"."+fieldName).arg(tableAlias+"."+fieldName.latin1()));
326 return false;
327 }
328 }
329
330 int tablePosition = -1;
331 if (!ts) {//try to find tableAlias
332 tablePosition = parseInfo.querySchema->tablePositionForAlias( tableName.latin1() );
333 if (tablePosition>=0) {
334 ts = parseInfo.querySchema->tables()->at(tablePosition);
335 if (ts) {
336// KexiDBDbg << " --it's a tableAlias.name" << endl;
337 }
338 }
339 }
340
341
342 if (ts) {
343 TQValueList<int> *positionsList = repeatedTablesAndAliases[ tableName ];
344 if (!positionsList) {
345 IMPL_ERROR(tableName + "." + fieldName + ", !positionsList ");
346 return false;
347 }
348
349 if (fieldName=="*") {
350 if (positionsList->count()>1) {
351 setError(i18n("Ambiguous \"%1.*\" expression").arg(tableName),
352 i18n("More than one \"%1\" table or alias defined").arg(tableName));
353 return false;
354 }
355 parseInfo.querySchema->addAsterisk( new QueryAsterisk(parseInfo.querySchema, ts) );
356 }
357 else {
358// KexiDBDbg << " --it's a table.name" << endl;
359 Field *realField = ts->field(fieldName);
360 if (realField) {
361 // check if table or alias is used twice and both have the same column
362 // (so the column is ambiguous)
363 int numberOfTheSameFields = 0;
364 for (TQValueList<int>::iterator it = positionsList->begin();
365 it!=positionsList->end();++it)
366 {
367 TableSchema *otherTS = parseInfo.querySchema->tables()->at(*it);
368 if (otherTS->field(fieldName))
369 numberOfTheSameFields++;
370 if (numberOfTheSameFields>1) {
371 setError(i18n("Ambiguous \"%1.%2\" expression").arg(tableName).arg(fieldName),
372 i18n("More than one \"%1\" table or alias defined containing \"%2\" field")
373 .arg(tableName).arg(fieldName));
374 return false;
375 }
376 }
377
378 parseInfo.querySchema->addField(realField, tablePosition);
379 }
380 else {
381 setError(i18n("Field not found"), i18n("Table \"%1\" has no \"%2\" field")
382 .arg(tableName).arg(fieldName));
383 return false;
384 }
385 }
386 }
387 else {
388 tableNotFoundError(tableName);
389 return false;
390 }
391 }
392#endif
393 return true;
394}
395
397#define CLEANUP \
398 delete colViews; \
399 delete tablesList; \
400 delete options
401
402QuerySchema* buildSelectQuery(
403 QuerySchema* querySchema, NArgExpr* colViews, NArgExpr* tablesList,
404 SelectOptionsInternal* options )
405{
406 ParseInfo parseInfo(querySchema);
407
408 //-------tables list
409// assert( tablesList ); //&& tablesList->exprClass() == KexiDBExpr_TableList );
410
411 uint columnNum = 0;
412/*TODO: use this later if there are columns that use database fields,
413 e.g. "SELECT 1 from table1 t, table2 t") is ok however. */
414 //used to collect information about first repeated table name or alias:
415// TQDict<char> tableNamesAndTableAliases(997, false);
416// TQString repeatedTableNameOrTableAlias;
417 if (tablesList) {
418 for (int i=0; i<tablesList->args(); i++, columnNum++) {
419 BaseExpr *e = tablesList->arg(i);
420 VariableExpr* t_e = 0;
421 TQCString aliasString;
422 if (e->exprClass() == KexiDBExpr_SpecialBinary) {
423 BinaryExpr* t_with_alias = e->toBinary();
424 assert(t_with_alias);
425 assert(t_with_alias->left()->exprClass() == KexiDBExpr_Variable);
426 assert(t_with_alias->right()->exprClass() == KexiDBExpr_Variable
427 && (t_with_alias->token()==AS || t_with_alias->token()==0));
428 t_e = t_with_alias->left()->toVariable();
429 aliasString = t_with_alias->right()->toVariable()->name.latin1();
430 }
431 else {
432 t_e = e->toVariable();
433 }
434 assert(t_e);
435 TQCString tname = t_e->name.latin1();
436 TableSchema *s = parser->db()->tableSchema(TQString(tname));
437 if(!s) {
438 setError(//i18n("Field List Error"),
439 i18n("Table \"%1\" does not exist").arg(TQString(tname)));
440 // yyerror("fieldlisterror");
441 CLEANUP;
442 return 0;
443 }
444 TQCString tableOrAliasName;
445 if (!aliasString.isEmpty()) {
446 tableOrAliasName = aliasString;
447// KexiDBDbg << "- add alias for table: " << aliasString << endl;
448 } else {
449 tableOrAliasName = tname;
450 }
451 // 1. collect information about first repeated table name or alias
452 // (potential ambiguity)
453 TQValueList<int> *list = parseInfo.repeatedTablesAndAliases[tableOrAliasName];
454 if (list) {
455 //another table/alias with the same name
456 list->append( i );
457// KexiDBDbg << "- another table/alias with name: " << tableOrAliasName << endl;
458 }
459 else {
460 list = new TQValueList<int>();
461 list->append( i );
462 parseInfo.repeatedTablesAndAliases.insert( tableOrAliasName, list );
463// KexiDBDbg << "- first table/alias with name: " << tableOrAliasName << endl;
464 }
465 /* if (repeatedTableNameOrTableAlias.isEmpty()) {
466 if (tableNamesAndTableAliases[tname])
467 repeatedTableNameOrTableAlias=tname;
468 else
469 tableNamesAndTableAliases.insert(tname, (const char*)1);
470 }
471 if (!aliasString.isEmpty()) {
472 KexiDBDbg << "- add alias for table: " << aliasString << endl;
473 // querySchema->setTableAlias(columnNum, aliasString);
474 //2. collect information about first repeated table name or alias
475 // (potential ambiguity)
476 if (repeatedTableNameOrTableAlias.isEmpty()) {
477 if (tableNamesAndTableAliases[aliasString])
478 repeatedTableNameOrTableAlias=aliasString;
479 else
480 tableNamesAndTableAliases.insert(aliasString, (const char*)1);
481 }
482 }*/
483// KexiDBDbg << "addTable: " << tname << endl;
484 querySchema->addTable( s, aliasString );
485 }
486 }
487
488 /* set parent table if there's only one */
489// if (parser->select()->tables()->count()==1)
490 if (querySchema->tables()->count()==1)
491 querySchema->setMasterTable(querySchema->tables()->first());
492
493 //-------add fields
494 if (colViews) {
495 BaseExpr *e;
496 columnNum = 0;
497 const uint colCount = colViews->list.count();
498// for (BaseExpr::ListIterator it(colViews->list);(e = it.current()); columnNum++)
499 colViews->list.first();
500 for (; columnNum<colCount; columnNum++) {
501 e = colViews->list.current();
502 bool moveNext = true; //used to avoid ++it when an item is taken from the list
503 BaseExpr *columnExpr = e;
504 VariableExpr* aliasVariable = 0;
505 if (e->exprClass() == KexiDBExpr_SpecialBinary && e->toBinary()
506 && (e->token()==AS || e->token()==0))
507 {
508 //KexiDBExpr_SpecialBinary: with alias
509 columnExpr = e->toBinary()->left();
510 // isFieldWithAlias = true;
511 aliasVariable = e->toBinary()->right()->toVariable();
512 if (!aliasVariable) {
513 setError(i18n("Invalid alias definition for column \"%1\"")
514 .arg(columnExpr->toString())); //ok?
515 CLEANUP;
516 return 0;
517 }
518 }
519
520 const int c = columnExpr->exprClass();
521 const bool isExpressionField =
522 c == KexiDBExpr_Const
523 || c == KexiDBExpr_Unary
524 || c == KexiDBExpr_Arithm
525 || c == KexiDBExpr_Logical
526 || c == KexiDBExpr_Relational
527 || c == KexiDBExpr_Const
528 || c == KexiDBExpr_Function
529 || c == KexiDBExpr_Aggregation;
530
531 if (c == KexiDBExpr_Variable) {
532 //just a variable, do nothing, addColumn() will handle this
533 }
534 else if (isExpressionField) {
535 //expression object will be reused, take, will be owned, do not destroy
536// KexiDBDbg << colViews->list.count() << " " << it.current()->debugString() << endl;
537 colViews->list.take(); //take() doesn't work
538 moveNext = false;
539 }
540 else if (aliasVariable) {
541 //take first (left) argument of the special binary expr, will be owned, do not destroy
542 e->toBinary()->m_larg = 0;
543 }
544 else {
545 setError(i18n("Invalid \"%1\" column definition").arg(e->toString())); //ok?
546 CLEANUP;
547 return 0;
548 }
549
550 if (!addColumn( parseInfo, columnExpr )) {
551 CLEANUP;
552 return 0;
553 }
554
555 if (aliasVariable) {
556// KexiDBDbg << "ALIAS \"" << aliasVariable->name << "\" set for column "
557// << columnNum << endl;
558 querySchema->setColumnAlias(columnNum, aliasVariable->name.latin1());
559 }
560 /* if (e->exprClass() == KexiDBExpr_SpecialBinary && dynamic_cast<BinaryExpr*>(e)
561 && (e->type()==AS || e->type()==0))
562 {
563 //also add alias
564 VariableExpr* aliasVariable =
565 dynamic_cast<VariableExpr*>(dynamic_cast<BinaryExpr*>(e)->right());
566 if (!aliasVariable) {
567 setError(i18n("Invalid column alias definition")); //ok?
568 return 0;
569 }
570 kdDebug() << "ALIAS \"" << aliasVariable->name << "\" set for column "
571 << columnNum << endl;
572 querySchema->setColumnAlias(columnNum, aliasVariable->name.latin1());
573 }*/
574
575 if (moveNext) {
576 colViews->list.next();
577// ++it;
578 }
579 }
580 }
581 //----- SELECT options
582 if (options) {
583 //----- WHERE expr.
584 if (options->whereExpr) {
585 if (!options->whereExpr->validate(parseInfo)) {
586 setError(parseInfo.errMsg, parseInfo.errDescr);
587 CLEANUP;
588 return 0;
589 }
590 querySchema->setWhereExpression(options->whereExpr);
591 }
592 //----- ORDER BY
593 if (options->orderByColumns) {
594 OrderByColumnList &orderByColumnList = querySchema->orderByColumnList();
595 OrderByColumnInternal::ListConstIterator it = options->orderByColumns->constEnd();
596 uint count = options->orderByColumns->count();
597 --it;
598 for (;count>0; --it, --count)
599 /*opposite direction due to parser specifics*/
600 {
601 //first, try to find a column name or alias (outside of asterisks)
602 QueryColumnInfo *columnInfo = querySchema->columnInfo( (*it).aliasOrName, false/*outside of asterisks*/ );
603 if (columnInfo) {
604 orderByColumnList.appendColumn( *columnInfo, (*it).ascending );
605 }
606 else {
607 //failed, try to find a field name within all the tables
608 if ((*it).columnNumber != -1) {
609 if (!orderByColumnList.appendColumn( *querySchema,
610 (*it).ascending, (*it).columnNumber-1 ))
611 {
612 setError(i18n("Could not define sorting - no column at position %1")
613 .arg((*it).columnNumber));
614 CLEANUP;
615 return 0;
616 }
617 }
618 else {
619 Field * f = querySchema->findTableField((*it).aliasOrName);
620 if (!f) {
621 setError(i18n("Could not define sorting - "
622 "column name or alias \"%1\" does not exist").arg((*it).aliasOrName));
623 CLEANUP;
624 return 0;
625 }
626 orderByColumnList.appendField( *f, (*it).ascending );
627 }
628 }
629 }
630 }
631 }
632
633// KexiDBDbg << "Select ColViews=" << (colViews ? colViews->debugString() : TQString())
634// << " Tables=" << (tablesList ? tablesList->debugString() : TQString()) << endl;
635
636 CLEANUP;
637 return querySchema;
638}
639
640#undef CLEANUP
641
A base class for all expressions.
Definition expression.h:67
virtual TQString toString(QuerySchemaParameterValueListIterator *params=0)=0
TableSchema * tableSchema(int tableId)
Field * field(uint id)
Definition fieldlist.h:76
Meta-data for a field.
Definition field.h:73
virtual TableSchema * table() const
Definition field.cpp:300
A base class N-argument operation.
Definition expression.h:130
KexiDB::OrderByColumnList provides list of sorted columns for a query schema.
void appendColumn(QueryColumnInfo &columnInfo, bool ascending=true)
void appendField(Field &field, bool ascending=true)
Provides detailed i18n'ed error description about the Parser .
Definition parser.h:43
TQString type()
Definition parser.h:69
TQString error()
Definition parser.h:74
Parser for SQL statements.
Definition parser.h:113
ParserError error() const
Definition parser.cpp:73
Connection * db() const
Definition parser.cpp:71
void clear()
rests results
Definition parser.cpp:131
TQString statement() const
Definition parser.cpp:75
@ OP_Select
Alter an existing table.
Definition parser.h:125
@ OP_Error
No statement parsed or reseted.
Definition parser.h:122
bool isReservedKeyword(const char *str)
Definition parser.cpp:110
OPCode operation() const
Definition parser.cpp:42
KexiDB::QueryAsterisk class encapsulates information about single asterisk in query definition.
Helper class that assigns additional information for the column in a query.
Definition queryschema.h:49
KexiDB::QuerySchema provides information about database query.
void addTable(TableSchema *table, const TQCString &alias=TQCString())
void setWhereExpression(BaseExpr *expr)
OrderByColumnList & orderByColumnList() const
void setMasterTable(TableSchema *table)
Field * findTableField(const TQString &tableOrTableAndFieldName) const
TableSchema::List * tables() const
QueryColumnInfo * columnInfo(const TQString &identifier, bool expanded=true)
void setColumnAlias(uint position, const TQCString &alias)
TQPtrListIterator< TableSchema > ListIterator
Iterator for tables list.
Definition tableschema.h:46
Variables like fieldname or tablename.fieldname
Definition expression.h:245
TableSchema * tableForQueryAsterisk
Definition expression.h:282
High-level database connectivity library with database backend drivers.