#include <math.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <errno.h>
#define __USE_XOPEN_EXTENDED
#include <string.h>
#include "interpreter.h"
#include "lookup3.h"
#include "hash.h"
#include "aggregators.h"
#include "json.h"
#define MAX_INFIELDS_PER_AGGREGATOR 2
#define MAX_CLUMPS_INFINITE -1
struct clump
{
hnode_t hash_node;
char **key_values;
struct clump *next, *prev; /* for doing LRU eviction */
double aggregator_data[]; /* use doubles to get double alignment */
};
struct str_ref
{
int offset;
int len;
bool is_set;
};
struct agg_instance
{
struct aggregator *agg;
char *output_field_name;
int num_input_fields;
int input_fields[MAX_INFIELDS_PER_AGGREGATOR];
void *config_data;
};
struct collate_state
{
int max_clumps;
bool incremental;
int num_agg_instances;
struct agg_instance *agg_instances;
int cube_max;
char *cube_default;
hash_t *clump_table;
int num_interesting_fields;
int num_key_fields;
char **interesting_field_names;
int interesting_field;
struct str_ref *interesting_fields;
char **tmp_interesting_vals;
double *tmp_double_vals;
int next_clump;
int total_available_clumps;
int clump_size;
char *available_clumps;
struct clump *clumps_head, *clumps_tail;
};
void dump_clump(struct clump *clump, struct collate_state *cs)
{
fputc('{', stdout);
int i = 0;
for(i = 0; i < cs->num_key_fields; i++)
{
if(i != 0) putc(',', stdout);
char *val = clump->key_values[i];
if(val)
printf("\"%s\":\"%s\"", cs->interesting_field_names[i], val);
else
printf("\"%s\":null", cs->interesting_field_names[i]);
}
char *agg_data = (char*)&clump->aggregator_data[0];
for(int j = 0; j < cs->num_agg_instances; j++)
{
struct agg_instance *agg_inst = &cs->agg_instances[j];
if(i != 0) putc(',', stdout);
printf("\"%s\":", agg_inst->output_field_name);
agg_inst->agg->dump_func(agg_inst->config_data, agg_data);
agg_data += agg_inst->agg->data_size;
}
fputs("}\n", stdout);
}
/* use this itty bitty piece of global data so our hash functions know how many
* strings to expect. */
int num_key_fields;
int hash_comp_func(const void *_k1, const void *_k2)
{
char **k1 = (char**)_k1;
char **k2 = (char**)_k2;
for(int i = 0; i < num_key_fields; i++)
{
if(k1[i] == NULL || k2[i] == NULL)
{
if(k1[i] == k2[i])
return 0;
else if(k1[i] == NULL)
return -1;
else
return 1;
}
else
{
int cmp = strcmp(k1[i], k2[i]);
if(cmp != 0)
return cmp;
}
}
return 0;
}
hash_val_t hash_func(const void *_k)
{
char **k = (char**)_k;
int hash = 0;
for(int i = 0; i < num_key_fields; i++)
if(k[i])
hash = hashlittle(k[i], strlen(k[i]), hash);
return hash;
}
/*
* This callback will be called every time the parser parses a string.
* In this callback, however, we are only concerned with strings that
* are keys of the top-level object, so we immediately bail unless the
* parse stack is two deep.
*
* When we are parsing a key to the top-level object, we see if the
* string matches any of the keys we care about (because they are key
* fields or we are aggregating on them). If so, we save that information
* in our collate_state.
*/
void string_callback(struct parse_state *parse_state, void *_state)
{
struct collate_state *state = _state;
struct parse_stack_frame *frame;
if(parse_state->parse_stack_length == 2) // object, string
{
frame = &parse_state->parse_stack[parse_state->parse_stack_length-1];
/* grab offsets for the string and adjust them to ditch the quotes */
int start_off = frame->start_offset - parse_state->buffer->base_offset;
int one_past_end_off = parse_state->offset - parse_state->buffer->base_offset;
start_off++;
one_past_end_off--;
/* NULL-terminate the parsed string by writing a null into the buffer */
parse_state->buffer->buf[one_past_end_off] = '\0';
/* str is now our parsed string */
char *str = (char*)parse_state->buffer->buf + start_off;
/* Is this an interesting field? If so, save the offset into the
* table of interesting strings */
int i;
for(i = 0; state->interesting_field_names[i] != NULL; i++)
{
if(strcmp(str, state->interesting_field_names[i]) == 0)
{
state->interesting_field = i;
break;
}
}
/* if this wasn't an interesting string, use -1 to note that */
if(state->interesting_field_names[i] == NULL)
state->interesting_field = -1;
}
}
/*
* This callback will be called every time a value of any kind is parsed.
* We only want to pay attention to string or integer values in the top-level
* object, so we bail if the stack is the wrong depth (we're parsing a value
* in a sub-hash) or it's the wrong type (object, array, true, false, or null)
*
* If this is a top-level scalar value and we've just seen a key for a field
* we care about, we insert pointers to this value into a table of interesting values
* for processing once we hit the end of the record.
*/
void value_callback(struct parse_state *parse_state, void *_state)
{
struct collate_state *state = _state;
struct parse_stack_frame *frame;
if(parse_state->parse_stack_length == 2 && state->interesting_field != -1) // object, value
{
frame = &parse_state->parse_stack[parse_state->parse_stack_length-1];
int start_off = frame->start_offset;
int one_past_end_off = parse_state->offset;
/* if this is an object, array, true, false, or null, then bail */
int ch = parse_state->buffer->buf[start_off - parse_state->buffer->base_offset];
if(ch == '{' || ch == '[' || ch == 't' || ch == 'f' || ch == 'n')
return;
/* if this is a string, ditch the quotation marks */
if(ch == '"')
{
start_off++;
one_past_end_off--;
}
/* NULL-terminate the value (for later) */
char save = parse_state->buffer->buf[one_past_end_off - parse_state->buffer->base_offset];
parse_state->buffer->buf[one_past_end_off - parse_state->buffer->base_offset] = '\0';
/* save this value in our table of interesting values */
struct str_ref *field = &state->interesting_fields[state->interesting_field];
field->offset = start_off;
field->len = one_past_end_off - start_off;
field->is_set = true;
parse_state->buffer->buf[one_past_end_off - parse_state->buffer->base_offset] = save;
}
}
struct clump *find_or_create_clump(struct collate_state *state, char *key_vals[])
{
/* do the hash lookup based on key_vals */
struct clump *clump = (struct clump*)hash_lookup(state->clump_table, key_vals);
if(clump)
{
/* remove this clump from wherever it is in the LRU list */
if(clump->next) clump->next->prev = clump->prev;
else state->clumps_tail = clump->prev;
if(clump->prev) clump->prev->next = clump->next;
else state->clumps_head = clump->next;
}
else
{
/* this clump doesn't exist in the table -- we'll have to create it */
/* first find the memory. if we're on a fixed number of clumps and
* we've hit that limit, evict. otherwise, allocate. */
if(state->max_clumps != MAX_CLUMPS_INFINITE &&
hash_count(state->clump_table) >= state->max_clumps)
{
/* do an LRU eviction */
clump = state->clumps_tail;
state->clumps_tail = state->clumps_tail->prev;
if(state->clumps_tail) state->clumps_tail->next = NULL;
else state->clumps_head = NULL;
if(!state->incremental)
dump_clump(clump, state);
hash_delete(state->clump_table, &clump->hash_node);
for(int i = 0; i < state->num_key_fields; i++)
free(clump->key_values[i]);
free(clump->key_values[state->num_key_fields]);
}
else
{
/* allocate a clump */
clump = malloc(state->clump_size);
}
/* now create a copy of the key (set of key fields) that will belong to the table */
clump->key_values = malloc(sizeof(char*) * (state->num_key_fields+1));
for(int i = 0; i < state->num_key_fields; i++)
{
if(key_vals[i])
clump->key_values[i] = strdup(key_vals[i]);
else
clump->key_values[i] = NULL;
}
clump->key_values[state->num_key_fields] = NULL;
/* now give all the aggregator instances a chance to init their data in the clump */
char *agg_data = (char*)&clump->aggregator_data[0];
for(int i = 0; i < state->num_agg_instances; i++)
{
struct agg_instance *agg_inst = &state->agg_instances[i];
agg_inst->agg->init_func(agg_inst->config_data, agg_data);
agg_data += agg_inst->agg->data_size;
}
/* insert this clump into the hash table */
hnode_init(&clump->hash_node, 0);
hash_insert(state->clump_table, &clump->hash_node, clump->key_values);
}
/* move this clump to the front of the LRU list */
clump->next = state->clumps_head;
clump->prev = NULL;
if(state->clumps_head) state->clumps_head->prev = clump;
else state->clumps_tail = clump;
state->clumps_head = clump;
return clump;
}
void find_and_add_to_clump(struct collate_state *state, char *vals[], double d_vals[])
{
struct clump *clump = find_or_create_clump(state, vals);
char *agg_data = (char*)&clump->aggregator_data[0];
for(int i = 0; i < state->num_agg_instances; i++)
{
char *agg_vals[MAX_INFIELDS_PER_AGGREGATOR];
double agg_d_vals[MAX_INFIELDS_PER_AGGREGATOR];
struct agg_instance *agg_inst = &state->agg_instances[i];
/* map the values to the ones that the aggregator cares about */
for(int j = 0; j < agg_inst->num_input_fields; j++)
{
agg_vals[j] = vals[agg_inst->input_fields[j]];
agg_d_vals[j] = d_vals[agg_inst->input_fields[j]];
}
/* run the aggregator's add callback! */
agg_inst->agg->add_func(agg_inst->config_data, agg_data, agg_vals, agg_d_vals);
agg_data += agg_inst->agg->data_size;
}
if(state->incremental)
dump_clump(clump, state);
}
/*
* This callback is called at the end of each object. If the object that's ending
* is the top-level object, this is where we do the work of finding or creating
* the bucket for this record and letting each aggregator instance aggregate.
*/
void object_callback(struct parse_state *parse_state, void *_state)
{
struct collate_state *state = _state;
if(parse_state->parse_stack_length == 1) // object
{
/* NULL-terminate all values */
char *buf = (char*)parse_state->buffer->buf - parse_state->buffer->base_offset;
for(int i = 0; i < state->num_interesting_fields; i++)
{
struct str_ref *field = &state->interesting_fields[i];
if(field->is_set)
buf[field->offset+field->len] = '\0';
}
/* first pack the interesting values into a table of char** */
char *vals[state->num_interesting_fields+1];
for(int i = 0; i < state->num_interesting_fields; i++)
{
struct str_ref *field = &state->interesting_fields[i];
if(field->is_set)
vals[i] = buf + field->offset;
else
vals[i] = NULL;
}
vals[state->num_interesting_fields] = NULL;
/* now try to convert each value into a double, for aggregators that want that.
* values that have no numeric data are represented as NAN */
double dbl_vals[state->num_interesting_fields];
for(int i = 0; i < state->num_interesting_fields; i++)
{
if(vals[i])
{
char *endp;
dbl_vals[i] = strtod(vals[i], &endp);
if(vals[i] == endp)
dbl_vals[i] = NAN;
}
else
dbl_vals[i] = NAN;
}
/* to support cubing, we use the binary representation of the numbers 0 -- cube_max
* as a power set. if a bit is 0, then the real value is used. if a bit is 1,
* the cube default is used. if we're not cubing, cube_max is 1 and we only use
* 0: the value for which all real values are used. */
for(int i = 0; i < state->cube_max; i++)
{
char *clump_vals[state->num_interesting_fields];
double dbl_clump_vals[state->num_interesting_fields];
for(int j = 0; j < state->num_interesting_fields; j++)
{
if((1 << j) & i)
{
clump_vals[j] = state->cube_default;
dbl_clump_vals[j] = NAN;
}
else
{
clump_vals[j] = vals[j];
dbl_clump_vals[j] = dbl_vals[j];
}
}
find_and_add_to_clump(state, clump_vals, dbl_clump_vals);
}
}
}
void init_parser(struct parse_state *state, struct grammar **g, FILE *input)
{
struct bc_read_stream *s = bc_rs_open_mem(json_bc);
*g = load_grammar(s);
bc_rs_close_stream(s);
alloc_parse_state(state);
init_parse_state(state, *g, input);
}
char usage[] =
"Usage: recs-collate <args> [<files>]\n"
" Collate records of input (or records from <files>) into output records.\n"
"\n"
"Arguments:\n"
" --key|-k <keys> Comma separated list of key fields.\n"
" --aggregator|-a <aggregators> Colon separated list of aggregate field specifiers.\n"
" See \"Aggregates\" section below.\n"
" --size|--sz|-n <number> Number of running clumps to keep (default is 1).\n"
" --adjacent|-a|-1 Keep exactly one running clump.\n"
" --perfect Never purge clumps until the end.\n"
" --cube See \"Cubing\" section below.\n"
" --cube-default See \"Cubing\" section below.\n"
" --incremental Output a record every time an input record is added\n"
" to a clump (instead of every time a clump is flushed).\n"
"\n"
"Help / Usage Options:\n"
" --help Bail and output this help screen.\n"
" --list-aggregators Bail and output a list of aggregators.\n"
" --show-aggregator <aggregator> Bail and output this aggregator's detailed usage.\n"
"\n"
"Aggregates:\n"
" Aggregates are specified as [<fieldname>=]<aggregator>[,<arguments>]. The\n"
" default field name is aggregator and arguments joined by underscores. See\n"
" --list-aggregators for a list of available aggregators.\n"
"\n"
"Cubing:\n"
" Instead of added one entry for each input record, we add 2 ** (number of key\n"
" fields), with every possible combination of fields replaced with the default\n"
" (which defaults to \"ALL\" but can be specified with --cube-default). This is\n"
" really supposed to be used with --perfect. If our key fields were x and y\n"
" then we'd get output records for {x = 1, y = 2}, {x = 1, y = ALL}, {x = ALL,\n"
" y = 2} and {x = ALL, y = ALL}.\n"
"\n"
"Examples:\n"
" Count clumps of adjacent lines with matching x fields.\n"
" recs-collate --adjacent --key x --aggregator count\n"
" Count number of each x field in the entire file.\n"
" recs-collate --perfect --key x --aggregator count\n"
" Count number of each x field in the entire file, including an \"ALL\" line.\n"
" recs-collate --perfect --key x --aggregator count --cube\n"
" Produce a cummulative sum of field profit up to each date\n"
" recs-collate --key date --incremental --aggregator profit_to_date=sum,profit\n"
" Produce record count for each date, hour pair\n"
" recs-collate --key date,hour --perfect --aggregator count\n";
void usage_err(char *fmt, ...)
{
va_list args;
fprintf(stderr, "recs-collate: ");
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
fprintf(stderr, usage);
exit(1);
}
struct interesting_field
{
char *name;
bool is_key;
} *fields;
int fields_len = 0, fields_size = 6;
int add_interesting_field(char *str, bool is_key)
{
for(int i = 0; i < fields_len; i++)
{
if(strcmp(str, fields[i].name) == 0)
{
if(is_key) fields[i].is_key = true;
return i;
}
}
RESIZE_ARRAY_IF_NECESSARY(fields, fields_size, fields_len+1);
struct interesting_field *new_field = &fields[fields_len++];
new_field->name = strdup(str);
new_field->is_key = is_key;
return fields_len-1;
}
void init_agg_instance(struct agg_instance *agg_inst, char *agg_str)
{
/* agg_str is in format: [<fieldname>=]<aggregator>[,<arguments>] */
char *ch;
if((ch = strchr(agg_str, '=')))
{
/* a fieldname was supplied */
*ch = '\0';
agg_inst->output_field_name = strdup(agg_str);
*ch = '=';
agg_str = ch+1;
}
else
{
/* no fieldname was supplied -- create the default one */
agg_inst->output_field_name = strdup(agg_str);
char *comma = agg_inst->output_field_name;
while((comma = strchr(comma, ',')))
{
*comma = '_';
}
}
if((ch = strchr(agg_str, ','))) *ch = '\0';
/* find the aggregator matching this name */
struct aggregator *aggregator = &aggregators[0];
for(; aggregator->name; aggregator++)
{
if(strcmp(agg_str, aggregator->name) == 0 ||
strcmp(agg_str, aggregator->shortname) == 0)
{
int num_fields = 0;
char *fields[MAX_INFIELDS_PER_AGGREGATOR];
agg_inst->agg = aggregator;
agg_inst->agg->parse_args_func(&agg_inst->config_data, ch+1, &num_fields, fields);
agg_inst->num_input_fields = num_fields;
/* are we already watching the fields the aggregator wants?
* if not, start watching them. */
for(int k = 0; k < num_fields; k++)
agg_inst->input_fields[k] = add_interesting_field(fields[k], false);
return;
}
}
usage_err("Couldn't find an aggregator named '%s'", agg_str);
}
int main(int argc, char *argv[])
{
struct parse_state state;
int agg_instances_size = 6;
int agg_instances_data_size = 0;
bool cube = false;
struct collate_state cs = {
.max_clumps = 1,
.incremental = false,
.num_agg_instances = 0,
.agg_instances = malloc(sizeof(*cs.agg_instances) * agg_instances_size),
.clump_table = hash_create(HASHCOUNT_T_MAX, hash_comp_func, hash_func),
.clumps_head = NULL,
.clumps_tail = NULL,
.cube_max = 1,
.cube_default = "ALL"
};
int files_len = 0, files_size = 5;
FILE **files = malloc(sizeof(*files) * files_size);
/* round up the size of each aggregator data to a multiple of sizeof(double) */
for(struct aggregator *agg = aggregators; agg->name; agg++)
agg->data_size = ceil((double)agg->data_size / sizeof(double)) * sizeof(double);
struct grammar *g = NULL;
fields = malloc(sizeof(*fields) * fields_size);
/* parse command-line options */
for(int i = 1; i < argc; i++)
{
char *arg = argv[i];
if(strcmp(arg, "--key") == 0 || strcmp(arg, "-k") == 0)
{
char *keys = argv[++i];
if(keys == NULL)
usage_err("argument '%s' must be followed by a list of keys", arg);
char *str = strtok(keys, ",");
do {
add_interesting_field(str, true);
} while((str = strtok(NULL, ",")));
}
else if(strcmp(arg, "--aggregator") == 0 || strcmp(arg, "-a") == 0)
{
char *aggs = argv[++i];
if(aggs == NULL)
usage_err("argument '%s' must be followed by a list of aggregators", arg);
char *agg = strtok(aggs, ":");
do {
RESIZE_ARRAY_IF_NECESSARY(cs.agg_instances, agg_instances_size,
cs.num_agg_instances+1);
struct agg_instance *agg_inst = &cs.agg_instances[cs.num_agg_instances++];
init_agg_instance(agg_inst, agg);
agg_instances_data_size += agg_inst->agg->data_size;
} while((agg = strtok(NULL, ":")));
}
else if(strcmp(arg, "--size") == 0 || strcmp(arg, "--sz") == 0 ||
strcmp(arg, "-n") == 0)
{
char *size_str = argv[++i];
if(size_str == NULL)
usage_err("argument '%s' must be followed by an integer", arg);
long int size = strtol(size_str, NULL, 10);
if(errno == EINVAL)
usage_err("parameter to '%s' argument was not a valid integer", arg);
if(size < 1)
usage_err("the size must be greater than 0");
cs.max_clumps = size;
}
else if(strcmp(arg, "--adjacent") == 0 || strcmp(arg, "-a") == 0 ||
strcmp(arg, "-1") == 0)
{
cs.max_clumps = 1;
}
else if(strcmp(arg, "--perfect") == 0)
{
cs.max_clumps = MAX_CLUMPS_INFINITE;
}
else if(strcmp(arg, "--incremental") == 0)
{
cs.incremental = true;
}
else if(strcmp(arg, "--cube") == 0)
{
cube = true;
}
else if(strcmp(arg, "--cube-default") == 0)
{
char *cube_default = argv[++i];
if(cube_default == NULL)
usage_err("argument '--cube-default' must be followed by a string");
cs.cube_default = strdup(cube_default);
}
else
{
/* interpret the argument as a filename */
FILE *f = fopen(arg, "r");
if(f)
{
RESIZE_ARRAY_IF_NECESSARY(files, files_size, files_len+1);
files[files_len++] = f;
}
else
{
usage_err("Couldn't open file '%s' for reading", arg);
}
}
}
if(files_len == 0)
files[files_len++] = stdin;
if(fields_len == 0)
usage_err("must specify --key or --aggregator");
cs.num_interesting_fields = fields_len;
cs.num_key_fields = 0;
cs.interesting_field_names = malloc(sizeof(*cs.interesting_field_names) *
(cs.num_interesting_fields+1));
for(int i = 0; i < fields_len; i++)
if(fields[i].is_key)
cs.interesting_field_names[cs.num_key_fields++] = fields[i].name;
int nonkey_field_num = 0;
for(int i = 0; i < fields_len; i++)
if(!fields[i].is_key)
cs.interesting_field_names[cs.num_key_fields + nonkey_field_num++] = fields[i].name;
num_key_fields = cs.num_key_fields;
if(cube)
{
cs.cube_max = 1 << cs.num_key_fields;
if(cs.max_clumps != MAX_CLUMPS_INFINITE && cs.max_clumps < cs.cube_max)
usage_err("when cubing, you must have at least 2 ** num_key_fields clumps");
}
/* adjust agg instance field names to reflect new field order */
for(int i = 0; i < cs.num_agg_instances; i++)
{
struct agg_instance *agg_inst = &cs.agg_instances[i];
for(int j = 0; j < agg_inst->num_input_fields; j++)
{
for(int k = 0; k < cs.num_interesting_fields; k++)
{
if(strcmp(cs.interesting_field_names[k],
fields[agg_inst->input_fields[j]].name) == 0)
{
agg_inst->input_fields[j] = k;
break;
}
}
}
}
cs.interesting_field_names[cs.num_interesting_fields] = NULL;
cs.interesting_field = -1;
cs.interesting_fields = malloc(sizeof(*cs.interesting_fields) * cs.num_interesting_fields);
cs.tmp_interesting_vals = malloc(sizeof(*cs.tmp_interesting_vals) * (cs.num_interesting_fields+1));
cs.tmp_interesting_vals[cs.num_key_fields] = NULL;
cs.tmp_double_vals = malloc(sizeof(*cs.tmp_double_vals) * cs.num_interesting_fields);
cs.clump_size = sizeof(struct clump) + agg_instances_data_size;
cs.total_available_clumps = 128;
cs.available_clumps = malloc(cs.clump_size * cs.total_available_clumps);
cs.next_clump = 0;
for(int i = 0; i < files_len; i++)
{
init_parser(&state, &g, files[i]);
register_callback(&state, "string", string_callback, &cs);
register_callback(&state, "value", value_callback, &cs);
register_callback(&state, "object", object_callback, &cs);
bool eof = false;
while(!eof)
{
for(int field = 0; field < cs.num_interesting_fields; field++)
cs.interesting_fields[field].is_set = false;
parse(&state, &eof);
reinit_parse_state(&state);
}
free_parse_state(&state);
}
hscan_t scan;
hash_scan_begin(&scan, cs.clump_table);
hnode_t *node;
while((node = hash_scan_next(&scan)))
{
if(!cs.incremental)
dump_clump((struct clump*)node, &cs);
hash_scan_delete(cs.clump_table, node);
}
free_grammar(g);
}