Files
bongo/src/agents/store/query-builder.c
T
pfelt 2b417a9c7d -- remove BongoArray and replace it with GArray
-- StrictCompile antispam and fix all the accompanying warnings
2009-07-18 18:09:53 +00:00

560 lines
16 KiB
C

/**
* \file QueryBuilder is used to translate the queries and other options various Store commands
* take into an SQL expression which can be run against the store database. In general, these
* are one specific class of query: querying storeobjects, filtering them, retrieving their
* various properties, etc.
*/
#include <memmgr.h>
#include <bongoutil.h>
#include "query-builder.h"
#include "query-parser.h"
#include "properties.h"
static int QueryExpressionToSQL(QueryBuilder *builder, struct expression *exp, BongoStringBuilder *sb);
static int QueryBuilderAddProperty(QueryBuilder *builder, const char *property, BOOL output);
/**
* Start a new Query Builder. This is used to turn queries and other data into
* complete SQL queries that can then be run.
* /param builder QueryBuilder instance we're setting up
* /return 0 on success, error codes otherwise
*/
int
QueryBuilderStart(QueryBuilder *builder)
{
memset(builder, 0, sizeof(QueryBuilder));
builder->order_prop = builder->int_query = builder->ext_query = NULL;
builder->order_direction = ORDER_NONE;
builder->output_mode = MODE_COLLECTIONS;
builder->properties = g_array_sized_new(FALSE, FALSE, sizeof(StorePropInfo *), 20);
builder->links = g_array_sized_new(FALSE, FALSE, sizeof(ExtraLink *), 10);
builder->parameters = g_array_sized_new(FALSE, FALSE, sizeof(QueryBuilder_Param *), 10);
return 0;
}
/**
* Finish the Query Builder we were using.
* \param builder QueryBuilder instance we're tearing down
*/
void
QueryBuilderFinish(QueryBuilder *builder)
{
unsigned int x;
StorePropInfo *newprop;
ExtraLink *link;
QueryBuilder_Param *param;
QueryParserFinish(&builder->internal_parser);
QueryParserFinish(&builder->external_parser);
/* free all the properties */
for(x=0;x<builder->properties->len;x++) {
newprop = g_array_index(builder->properties, StorePropInfo *, x);
MemFree(newprop);
}
g_array_free(builder->properties, TRUE);
/* free all the links */
for(x=0;x<builder->links->len;x++) {
link = g_array_index(builder->links, ExtraLink *, x);
MemFree(link);
}
g_array_free(builder->links, TRUE);
/* free all the parameters */
for(x=0;x<builder->parameters->len;x++) {
param = g_array_index(builder->parameters, QueryBuilder_Param *, x);
MemFree(param);
}
g_array_free(builder->parameters, TRUE);
}
/**
* Set the query string into the query we're building.
* \param builder the QueryBuilder instance we're dealing with
* \param query the query we want it to process
* \param internal whether or not the query has come from within Bongo code
* \return 0 on success, error codes otherwise.
*/
static int
QueryBuilderSetQuery(QueryBuilder *builder, char const *query, BOOL internal)
{
if (internal) {
builder->int_query = query;
} else {
builder->ext_query = query;
}
return 0;
}
int
QueryBuilderSetQuerySafe(QueryBuilder *builder, const char *query)
{
return QueryBuilderSetQuery(builder, query, TRUE);
}
int
QueryBuilderSetQueryUnsafe(QueryBuilder *builder, const char *query)
{
return QueryBuilderSetQuery(builder, query, FALSE);
}
/**
* Set the order of results and which property to order on
* \param builder the querybuilder we're using
* \param prop the property to order by
* \param asc whether or not the results should be in ascending order
* \return 0 for success, error codes otherwise
*/
int
QueryBuilderSetResultOrder(QueryBuilder *builder, char const *prop, BOOL asc)
{
builder->order_prop = prop;
if (asc)
builder->order_direction = ORDER_ASC;
else
builder->order_direction = ORDER_DESC;
// need to add the prop to the query in case it's not something
// we'd usually pull out.
return QueryBuilderAddProperty(builder, prop, FALSE);
}
/**
* Tell later users how much information we want about each document
* \param builder the querybuilder we're using
* \return 0 always successful
*/
int
QueryBuilderSetOutputMode(QueryBuilder *builder, QueryBuilder_OutputMode mode)
{
builder->output_mode = mode;
return 0;
}
/**
* Set the range on the query in terms of where we start and finish in
* the results. These refer to the individual "rows" of results, where
* results are only returned between these two positions.
* \param builder the querybuilder we're interested in
* \param start Index into the start of the results.
* \param end Index into the end of the result.
* \return 0 (always succeeds)
*/
int
QueryBuilderSetResultRange(QueryBuilder *builder, int start, int end)
{
builder->limit_start = start;
builder->limit_end = end;
return 0;
}
/**
* Add properties to be bound to the resulting SQL query. This is just
* a convenience really - we're not going to use this data within the
* builder, but it will be needed with the resulting SQL.
* Slightly ugly API, but other methods end up doing worse void casts.
*/
int
QueryBuilderAddParam(QueryBuilder *builder, int position,
QueryBuilder_ParamType type, int d1, uint64_t d2, char *d3)
{
QueryBuilder_Param *p;
p = MemNew0(QueryBuilder_Param, 1);
if (p == NULL) return -1;
p->position = position;
p->type = type;
switch (type) {
case TYPE_INT:
p->data.d_int = d1;
break;
case TYPE_INT64:
p->data.d_int64 = d2;
break;
case TYPE_TEXT:
p->data.d_text = d3;
break;
default:
return -1;
}
g_array_append_val(builder->parameters, p);
return 0;
}
/**
* Add extra results for a given query - usually, the value of a property for
* each document returned in the query
* \param builder the querybuilder we're interested in
* \param property the extra property we want
* \return 0 on success, errorcodes otherwise.
*/
int
QueryBuilderAddPropertyOutput(QueryBuilder *builder, const char *property)
{
return QueryBuilderAddProperty(builder, property, TRUE);
}
static int
QueryBuilderAddProperty(QueryBuilder *builder, char const *property, BOOL output)
{
StorePropInfo *newprop;
unsigned int i;
for (i=0; i < builder->properties->len; i++) {
StorePropInfo *prop = g_array_index(builder->properties, StorePropInfo *, i);
if (strcmp(prop->name, property) == 0) {
// we already have this property
prop->output |= output;
return 0;
}
}
newprop = MemNew0(StorePropInfo, 1);
newprop->type = 0;
newprop->name = (char *)property;
newprop->index = builder->properties->len;
StorePropertyFixup(newprop);
newprop->output = output;
g_array_append_val(builder->properties, newprop);
return 0;
}
static int
QueryBuilderPropertyToColumn(QueryBuilder *builder, BongoStringBuilder *sb, StorePropInfo *prop)
{
if (builder == NULL) return -1;
if ((prop->table_name != NULL) && (prop->column != NULL)) {
BongoStringBuilderAppendF(sb, "%s.%s", prop->table_name, prop->column);
} else {
BongoStringBuilderAppendF(sb, "prop_%d.value", prop->index);
}
return 0;
}
static int
QueryBuilderFindExpressionProps(QueryBuilder *builder, struct expression *exp)
{
int retcode;
// treat linked documents specially, because we need to join those in.
if (exp->op[0] == 'l') {
BOOL to = FALSE;
ExtraLink *link;
// asking for something to be linked. Sneakily, we'll replace the first
// argument with the link struct we create. Slightly naughty.
link = MemNew0(ExtraLink, 1);
if (!exp->exp1_const) return -1; // can't have a non-const here..
if (strncmp(exp->exp1, "to", 2) == 0) to = TRUE;
if (to) {
link->join_column = "doc_guid";
link->test_column = "related_guid";
} else {
link->join_column = "related_guid";
link->test_column = "doc_guid";
}
link->pos = builder->links->len;
// add the new link to our list of links
g_array_append_val(builder->links, link);
exp->exp1 = link; // FIXME: [linkhack] very hacky :(
return 0;
}
if (exp->exp1_const) {
if (QueryParser_IsProperty((char *)exp->exp1) == 0) {
QueryBuilderAddProperty(builder, (char *)exp->exp1, FALSE);
}
} else {
retcode = QueryBuilderFindExpressionProps(builder, exp->exp1);
if (retcode != 0) return retcode;
}
if (exp->exp2_const) {
if (QueryParser_IsProperty((char *)exp->exp2) == 0) {
QueryBuilderAddProperty(builder, (char *)exp->exp2, FALSE);
}
} else {
return QueryBuilderFindExpressionProps(builder, exp->exp2);
}
return 0;
}
int
QueryBuilderRun(QueryBuilder *builder)
{
int retcode = 0;
if (builder->int_query != NULL) {
if (QueryParserStart(&builder->internal_parser, builder->int_query, 50)) goto abort;
if (QueryParserRun(&builder->internal_parser)) goto abort;
if (QueryBuilderFindExpressionProps(builder, builder->internal_parser.start)) goto abort;
}
if (builder->ext_query != NULL) {
if (QueryParserStart(&builder->external_parser, builder->ext_query, 50)) goto abort;
if (QueryParserRun(&builder->external_parser)) goto abort;
if (QueryBuilderFindExpressionProps(builder, builder->external_parser.start)) goto abort;
}
goto finish;
abort:
retcode = -1;
finish:
return retcode;
}
/**
* Create the SQL query based on the information we've gathered so far and the queries we've parsed.
* All queries are of the form:
* SELECT <default rows>, <optional props> FROM storeobject, <other needed tables> WHERE <conditions>
* ORDER BY <some column> [ASC|DESC] LIMIT <some amount>;
* \param builder The builder instance we're using
* \param output a char * where we should place the output. Caller frees? (can we do this in Finish?)
* \return 0 on success, error codes otherwise.
*/
int
QueryBuilderCreateSQL(QueryBuilder *builder, char **output)
{
BongoStringBuilder b;
unsigned int i;
int ccode;
if (BongoStringBuilderInit(&b)) {
// unable to start... ick
return -1;
}
// basic start for all queries
BongoStringBuilderAppend(&b, "SELECT so.guid, so.collection_guid, so.imap_uid, so.filename, so.type, so.flags, so.size, so.time_created, so.time_modified");
// extra columns for additional properties
for (i=0; i < builder->properties->len; i++) {
StorePropInfo *prop = g_array_index(builder->properties, StorePropInfo *, i);
// only add those columns which we want returned.
BongoStringBuilderAppend(&b, ", ");
QueryBuilderPropertyToColumn(builder, &b, prop);
}
BongoStringBuilderAppend(&b, " FROM storeobject so");
// add in the tables that we need
if (builder->linkin_conversations) {
BongoStringBuilderAppend(&b, " INNER JOIN conversation c ON so.guid=c.guid");
}
for (i=0; i < builder->links->len; i++) {
ExtraLink *link = g_array_index(builder->links, ExtraLink *, i);
BongoStringBuilderAppendF(&b,
" INNER JOIN links link_%d ON so.guid=link_%d.%s",
i, i, link->join_column);
}
for (i=0; i < builder->properties->len; i++) {
StorePropInfo *prop = g_array_index(builder->properties, StorePropInfo *, i);
// only add those columns which we want returned.
if ((prop->table_name == NULL) && (prop->column == NULL)) {
// FIXME. Both following queries should use bound parameters really.
if (prop->type == STORE_PROP_EXTERNAL) {
BongoStringBuilderAppendF(&b,
" LEFT JOIN properties prop_%d ON so.guid=prop_%d.guid AND prop_%d.name=\"%s\"",
i, i, i, prop->name);
} else {
BongoStringBuilderAppendF(&b,
" LEFT JOIN properties prop_%d ON so.guid=prop_%d.guid AND prop_%d.intprop=%d",
i, i, i, (int)prop->type);
}
}
}
if (builder->int_query || builder->ext_query || (builder->properties->len > 0))
BongoStringBuilderAppend(&b, " WHERE ");
// add in any constraints specified on the various columns
if (builder->int_query) {
if (QueryExpressionToSQL(builder, builder->internal_parser.start, &b)) {
ccode = -2;
goto abort;
}
}
if (builder->int_query && builder->ext_query) {
BongoStringBuilderAppend(&b, " AND ");
}
if (builder->ext_query) {
if (QueryExpressionToSQL(builder, builder->external_parser.start, &b)) {
ccode = -3;
goto abort;
}
}
// set the order of the query if requested
if (builder->order_prop != NULL) {
StorePropInfo prop;
char *direction = (builder->order_direction == ORDER_ASC)?
"ASC" : "DESC";
BongoStringBuilderAppendF(&b, " ORDER BY ");
prop.name = (char *)builder->order_prop;
StorePropertyFixup(&prop);
// FIXME: do we also need the table alias here?
BongoStringBuilderAppendF(&b, " %s %s", prop.column, direction);
}
// set any limit on the results.
if (builder->limit_start > 0) {
BongoStringBuilderAppendF(&b, " LIMIT %d, %d",
builder->limit_start, builder->limit_end);
}
BongoStringBuilderAppend(&b, ";");
*output = MemStrdup(b.value);
BongoStringBuilderDestroy(&b);
return 0;
abort:
BongoStringBuilderDestroy(&b);
return ccode;
}
static int
QueryExpressionToSQL(QueryBuilder *builder, struct expression *exp, BongoStringBuilder *sb)
{
const char basic_ops[] = "&|<>=!~";
unsigned int i = 0;
int retcode = 0;
for (i=0; i < sizeof(basic_ops); i++) {
if (exp->op[0] == basic_ops[i]) {
BongoStringBuilderAppend(sb, "(");
if (exp->exp1_const) {
if (QueryParser_IsProperty((char *)exp->exp1) == 0) {
StorePropInfo prop;
prop.type = 0;
prop.name = (char *)exp->exp1;
StorePropertyFixup(&prop);
QueryBuilderPropertyToColumn(builder, sb, &prop);
} else {
BongoStringBuilderAppend(sb, (char *)exp->exp1);
}
} else {
retcode = QueryExpressionToSQL(builder, (struct expression *)exp->exp1, sb);
if (retcode) return retcode;
}
switch (exp->op[0]) {
case '&':
BongoStringBuilderAppend(sb, " AND ");
break;
case '|':
BongoStringBuilderAppend(sb, " OR ");
break;
case '~':
BongoStringBuilderAppend(sb, " & ");
break;
case '<':
case '>':
case '=':
BongoStringBuilderAppendF(sb, " %c ", exp->op[0]);
break;
case '!':
BongoStringBuilderAppend(sb, " != ");
break;
default:
// return 0;
break;
}
if (exp->exp2_const) {
if (QueryParser_IsProperty((char *)exp->exp2) == 0) {
StorePropInfo prop;
prop.type = 0;
prop.name = (char *)exp->exp2;
StorePropertyFixup(&prop);
QueryBuilderPropertyToColumn(builder, sb, &prop);
} else {
BongoStringBuilderAppend(sb, (char *)exp->exp2);
}
} else {
retcode = QueryExpressionToSQL(builder, (struct expression *)exp->exp2, sb);
if (retcode) return retcode;
}
BongoStringBuilderAppend(sb, ")");
return 0;
}
}
if (exp->op[0] == 'l') {
// FIXME: [linkhack] need to do this slightly better wrt. finding link
ExtraLink *link;
link = (ExtraLink *)exp->exp1;
BongoStringBuilderAppendF(sb, "(link_%d.%s = %s)", link->pos,
link->test_column, (char *)exp->exp2);
return 0;
}
if (exp->op[0] == '{') {
// FIXME: left-substring must also not have any sub-expressions.
StorePropInfo prop;
prop.type = 0;
prop.name = (char *)exp->exp1;
StorePropertyFixup(&prop);
BongoStringBuilderAppend(sb, "substr(");
QueryBuilderPropertyToColumn(builder, sb, &prop);
BongoStringBuilderAppendF(sb, ",1,%s)", (char *)exp->exp2);
return 0;
}
// no matching operator - parser out of step with the builder.
return -2;
}
#if 0
int
QueryBuilderTest(void)
{
QueryBuilder builder;
char *sql = NULL;
// no error checking here.. bad... :)
QueryBuilderStart(&builder);
//QueryBuilderSetQuerySafe(&builder, "& & = nmap.type 4096 ! nmap.guid ?3 = { nmap.document ?2 ?1");
//QueryBuilderSetQueryUnsafe(&builder, "& & & = nmap.type 2 l from 10 = { nmap.conversation.subject 5 \"hello\" | l to 6 l from 6");
QueryBuilderAddPropertyOutput(&builder, "alex.custom");
QueryBuilderAddPropertyOutput(&builder, "nmap.conversation.subject");
if (QueryBuilderRun(&builder)) {
printf("Couldn't parse the queries?\n");
} else if (QueryBuilderCreateSQL(&builder, &sql)) {
printf("Couldn't create the SQL\n");
} else {
printf("SQL: %s\n", sql);
}
QueryBuilderFinish(&builder);
if (sql) MemFree(sql);
printf("Done.\n");
return 0;
}
#endif