sources/cg_test_helpers.c (780 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #if defined(CQL_AMALGAM_LEAN) && !defined(CQL_AMALGAM_TEST_HELPERS) // stubs to avoid link errors cql_noexport void cg_test_helpers_main(ast_node *head) {} #else // Given a procedure, we can create a temp table that has the exact shape as the proc // We can then insert and select from the temp table to fake a result set // This file performs codegen for those procedures #include "cg_test_helpers.h" #include <stdint.h> #include "ast.h" #include "cg_common.h" #include "charbuf.h" #include "cql.h" #include "gen_sql.h" #include "list.h" #include "sem.h" #include "symtab.h" #include "crc64xz.h" #include "encoders.h" #define DUMMY_TABLE 1 // dummy_table attribute flag #define DUMMY_INSERT 2 // dummy_insert attribute flag #define DUMMY_SELECT 4 // dummy_select attribute flag #define DUMMY_RESULT_SET 8 // dummy_result_set attribute flag #define DUMMY_TEST 0x10 // dummy_test attribute flag #define DUMMY_TEST_INSERT_ROWS 2 // minimum number of rows inserted in table for dummy_test attribution static charbuf *cg_th_output; static charbuf *cg_th_decls; static charbuf* cg_th_procs; // dummy_test utility variable used to emit statements. static charbuf *gen_create_triggers; static charbuf *gen_drop_triggers; // All triggers per tables. This is used as part of dummy_test to help look up // all the triggers to emit static symtab *all_tables_with_triggers; // All indexes per tables. This is used as part of dummy_test to help look up // all the indexes to emit static symtab *all_tables_with_indexes; // Record the autotest attribute processed. This is used to figure out if there // will be code gen to write to the output file static int32_t helper_flags = 0; // hold all the table name, column name and column values provided by dummy_test node static symtab *dummy_test_infos = NULL; typedef struct dummy_test_info { list_item *found_tables; list_item *found_views; CSTR table_current; struct table_callbacks *callbacks; } dummy_test_info; static void find_all_table_nodes(dummy_test_info *info, ast_node *node); static void cg_dummy_test_populate(charbuf *gen_insert_tables, ast_node *table_ast, int32_t *dummy_value_seed); // The dummy_table, dummy_insert, dummy_select and dummy_result_set attributions // will reference the original procedure by name in a LIKE clause. In order to get its // result type, we need to emit a declaration for the proc because its body will not be // in the test helper file. This function tells us if we need to emit that declaration. static bool is_declare_proc_needed() { int32_t needed = DUMMY_TABLE | DUMMY_INSERT | DUMMY_SELECT | DUMMY_RESULT_SET; return !!(helper_flags & needed); } // Emit a declaration for the proc so that the signature is known by // the generated dummy procs. See above. static void cg_test_helpers_declare_proc(ast_node *ast) { bprintf(cg_th_decls, "\n"); gen_set_output_buffer(cg_th_decls); gen_declare_proc_from_create_proc(ast); bprintf(cg_th_decls, ";\n"); } static bool_t cg_test_helpers_force_if_not_exists( ast_node *_Nonnull ast, void *_Nullable context, charbuf *_Nonnull output) { bprintf(output, "IF NOT EXISTS "); return true; } // Emit an open proc which creates a temp table in the form of the original proc // Emit a close proc which drops the temp table static void cg_test_helpers_dummy_table(CSTR name) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC open_%s()\n", name); bprintf(cg_th_procs, "BEGIN\n"); bprintf(cg_th_procs, " CREATE TEMP TABLE test_%s(LIKE %s);\n", name, name); bprintf(cg_th_procs, "END;\n"); bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC close_%s()\n", name); bprintf(cg_th_procs, "BEGIN\n"); bprintf(cg_th_procs, " DROP TABLE test_%s;\n", name); bprintf(cg_th_procs, "END;\n"); } // Emit a dummy insert to the temp table using FROM ARGUMENTS static void cg_test_helpers_dummy_insert(CSTR name) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC insert_%s(LIKE %s)\n", name, name); bprintf(cg_th_procs, "BEGIN\n"); bprintf(cg_th_procs, " INSERT INTO test_%s FROM ARGUMENTS;\n", name); bprintf(cg_th_procs, "END;\n"); } // Emit a dummy select from the temp table which will have a result set // that matches that of the original proc static void cg_test_helpers_dummy_select(CSTR name) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC select_%s()\n", name); bprintf(cg_th_procs, "BEGIN\n"); bprintf(cg_th_procs, " SELECT * FROM test_%s;\n", name); bprintf(cg_th_procs, "END;\n"); } // Emit a procedure that takes in arguments by the shape of the procedure // and produces a result set static void cg_test_helpers_dummy_result_set(CSTR name) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC generate_%s_row(LIKE %s)\n", name, name); bprintf(cg_th_procs, "BEGIN\n"); bprintf(cg_th_procs, " DECLARE curs CURSOR LIKE %s;\n", name); bprintf(cg_th_procs, " FETCH curs FROM ARGUMENTS;\n"); bprintf(cg_th_procs, " OUT curs;\n"); bprintf(cg_th_procs, "END;\n"); } // Find all triggers on the table "table_or_view_name" then find all the tables those // triggers actions depend on. The tables in those triggers should be parf of dummy_test // codegen otherwise trigger creation stmt will fail in dummy_test. static void find_all_triggers_node(dummy_test_info *info, CSTR table_or_view_name) { symtab_entry *triggers_entry = symtab_find(all_tables_with_triggers, table_or_view_name); gen_sql_callbacks callbacks; init_gen_sql_callbacks(&callbacks); callbacks.if_not_exists_callback = cg_test_helpers_force_if_not_exists; // We can safely visit all the triggers because we know we visit any given table only once if (triggers_entry) { // We collect this table as having triggers. Later we'll use this datastructure to emit // those triggers. bytebuf *buf = (bytebuf *)triggers_entry->val; ast_node **items = (ast_node **)buf->ptr; int32_t count = buf->used / sizeof(*items); for (int32_t i = 0; i < count; i++) { EXTRACT_ANY_NOTNULL(create_trigger_stmt, items[i]); // emit create trigger stmt gen_set_output_buffer(gen_create_triggers); gen_statement_with_callbacks(create_trigger_stmt, &callbacks); bprintf(gen_create_triggers, ";\n"); // emit drop trigger stmt gen_set_output_buffer(gen_drop_triggers); EXTRACT_NOTNULL(trigger_body_vers, create_trigger_stmt->right); EXTRACT_NOTNULL(trigger_def, trigger_body_vers->left); EXTRACT_ANY_NOTNULL(trigger_name_ast, trigger_def->left); EXTRACT_STRING(trigger_name, trigger_name_ast); bprintf(gen_drop_triggers, "DROP TRIGGER IF EXISTS %s;\n", trigger_name); // Now we need to find all the tables referenced in the triggers, because those tables // should also to be part of tables emit by dummy_test. Otherwise the triggers statement // will be referencing non existent table in dummy_test. continue_find_table_node(info->callbacks, create_trigger_stmt); } } } // - looks up all table relationships instead of just tables reference in a proc (follows the FKs) // - looks up drop table statements // - looks up triggers, and then the tables referenced in those triggers static void found_table_or_view(CSTR _Nonnull table_or_view_name, ast_node *_Nonnull table_or_view, void *_Nullable context) { Contract(table_or_view); dummy_test_info *info = (dummy_test_info *)context; bool deleted = table_or_view->sem->delete_version > 0; // tables/views that are deleted have no business appearing in the dummy test output if (!deleted) { // Now let's walk through the new found table (table_or_view_name) to find all the tables it // depends on. This is to find the FKs inside it. Note that we don't have to check for // cycles because the walker driving all of this already does that, we just go. continue_find_table_node(info->callbacks, table_or_view); // Items will naturally be inserted at the front of the list because add_item_to_list always adds // at the head. We don't want duplicates so we need to check. We do want newly found items to // go to the head because as visit things we always want it to be the case that dependencies we // visit later end up at the front. So if A depends on B then B will be first in the list. // Note tables do not directly depend on views so what's going to happen here is that // we will follow the FK chain and the deepest table will emitted first, hence be at the tail of the list. // Now the thing is one of those tables might have a trigger...the trigger itself could have // additional dependencies such as views. This is ok, this is sort of an indirect table to view // dependency but the thing is in this case the view must be created AFTER the tables not before // to manage this we keep a view list and a table list which we will stitch together at the end // so that all the views are after all the tables // This callback is invoked exactly once per table/view by the walker so we already know we // have to add the item to the list, we don't need to keep our own state. // note by now we've already visited and added things inside us so our dependencies are already in the list if (is_ast_create_view_stmt(table_or_view)) { add_item_to_list(&info->found_views, table_or_view); } else { add_item_to_list(&info->found_tables, table_or_view); } // Find all triggers on the table "table_or_view_name" then find all of the tables and triggers // referenced by them. These must come after the table itself has been analyzed find_all_triggers_node(info, table_or_view_name); } } static void find_all_table_nodes(dummy_test_info *info, ast_node *node) { table_callbacks callbacks = { .callback_any_table = found_table_or_view, .callback_any_view = found_table_or_view, .callback_context = info, .notify_table_or_view_drops = true, .notify_fk = true, .notify_triggers = true, }; info->callbacks = &callbacks; find_table_refs(&callbacks, node); // stitch the views to the tables to make one list, views first for (list_item *item = info->found_views; item; item = item->next) { if (!item->next) { item->next = info->found_tables; info->found_tables = info->found_views; break; } } // this shouldn't be used after it's been linked in info->found_views = NULL; } // Format the value in node accordingly to the node type. The semantic analysis // has already made sure the ast node type matches the column type in the table static void cg_dummy_test_column_value(charbuf *output, ast_node *value) { if (is_ast_uminus(value)) { Contract(is_ast_num(value->left)); bprintf(output, "%s", "-"); value = value->left; } if (is_ast_str(value)) { EXTRACT_STRING(lit, value); bprintf(output, "%s", lit); } else if (is_ast_num(value)) { EXTRACT_NUM_VALUE(lit, value); bprintf(output, "%s", lit); } else if (is_ast_null(value)) { bprintf(output, "NULL"); } else { Contract(is_ast_blob(value)); EXTRACT_BLOBTEXT(lit, value); bprintf(output, "%s", lit); } } // Find the parent column referenced in the foreign key statement by child table // "table_name" and column "column_name". We use this function to find parent // column to do some validation to avoid foreign key violations in insert statement // we emit. static void find_parent_column( ast_node *_Nullable *_Nonnull referenced_table_ast, CSTR _Nullable *_Nonnull referenced_column, CSTR table_name, CSTR column_name) { ast_node *table_ast = find_table_or_view_even_deleted(table_name); Contract(is_ast_create_table_stmt(table_ast)); EXTRACT_NOTNULL(col_key_list, table_ast->right); *referenced_table_ast = NULL; *referenced_column = NULL; for (ast_node *col_keys = col_key_list; col_keys; col_keys = col_keys->right) { if (is_ast_col_def(col_keys->left)) { // the column might be marked as an FK by the form col REFERENCES ref_table(ref_col) // to verify this we need to know that: // 1. col matches the required name // 2. there is an fk column attribute // if so we can get the referenced name and column from that attribute EXTRACT_NOTNULL(col_def, col_keys->left); EXTRACT_NOTNULL(col_def_type_attrs, col_def->left); EXTRACT_ANY(attrs, col_def_type_attrs->right); EXTRACT_NOTNULL(col_def_name_type, col_def_type_attrs->left); EXTRACT_STRING(name, col_def_name_type->left); if (!Strcasecmp(name, column_name)) { for (ast_node *attr = attrs; attr; attr = attr->right) { if (is_ast_col_attrs_fk(attr)) { EXTRACT_NOTNULL(fk_target_options, attr->left); EXTRACT_NOTNULL(fk_target, fk_target_options->left); EXTRACT_STRING(ref_table_name, fk_target->left); EXTRACT_NAMED_NOTNULL(ref_list, name_list, fk_target->right); EXTRACT_STRING(ref_col_name, ref_list->left); Contract(!ref_list->right); // it must be a list of one because its attribute form *referenced_table_ast = find_table_or_view_even_deleted(ref_table_name); *referenced_column = ref_col_name; return; } } } } else if (is_ast_fk_def(col_keys->left)) { // In the general case we're looking for an FK constraint that has this column // if we find such a constraint (ast_fk_def) then we look at the name list // for the required column, if present we extract the referenced table // and the corresponding referenced column. EXTRACT_NOTNULL(fk_def, col_keys->left); EXTRACT_NOTNULL(fk_info, fk_def->right); EXTRACT_NOTNULL(name_list, fk_info->left); int32_t column_index = 0; bool_t found = 0; for (ast_node *list = name_list; list; list = list->right) { EXTRACT_STRING(name, list->left); if (!Strcasecmp(name, column_name)) { found = 1; break; } column_index++; } if (found) { // All we need to do now is find the referenced name list // and skip to the column_index entry to get the corresponding // referenced name. The table name is sitting there for us // on a silver platter. EXTRACT_NOTNULL(fk_target_options, fk_info->right); EXTRACT_NOTNULL(fk_target, fk_target_options->left); EXTRACT_STRING(referenced_table, fk_target->left); EXTRACT_ANY_NOTNULL(fk_name_list, fk_target->right); int32_t index = 0; while (index < column_index) { fk_name_list = fk_name_list->right; index++; } Invariant(fk_name_list); EXTRACT_STRING(fk_col_name, fk_name_list->left); *referenced_table_ast = find_table_or_view_even_deleted(referenced_table); *referenced_column = fk_col_name; return; } } } } // make sure a value is within 1 and DUMMY_TEST_INSERT_ROWS static int32_t cg_validate_value_range(int32_t value) { if (1 <= value && value <= DUMMY_TEST_INSERT_ROWS) { return value; } else { return (value % DUMMY_TEST_INSERT_ROWS) + 1; } } // Emit a value of a parent column referenced by child column "column_name". // It allows the insert statement of a child table to include the column value // from the parent table. // This is useful to make sure a value provided by the user in dummy_test info // is actually included. // the parent column might have multiple values available. We use "index" to specify // the index of the one we want to emit. // e.g: Foo table has a foreign key column 'A' referencing column 'B' on the table Bar. // If a value for column 'B' of table Bar was specified in dummy_test info then that // value will be populated to column 'B' of table Foo static void cg_parent_column_value(charbuf *output, CSTR table_name, CSTR column_name, int32_t index) { ast_node *referenced_table_ast; CSTR referenced_column; find_parent_column(&referenced_table_ast, &referenced_column, table_name, column_name); if (referenced_table_ast) { CSTR referenced_table_name = referenced_table_ast->sem->sptr->struct_name; symtab_entry *referenced_table_entry = symtab_find(dummy_test_infos, referenced_table_name); if (referenced_table_entry) { symtab *fk_col_name_buf = (symtab *)referenced_table_entry->val; symtab_entry *fk_column_values_entry = symtab_find(fk_col_name_buf, referenced_column); if (fk_column_values_entry) { bytebuf *fk_column_values = (bytebuf *)fk_column_values_entry->val; ast_node **list = (ast_node **)fk_column_values->ptr; int32_t size = fk_column_values->used / sizeof(void *); cg_dummy_test_column_value(output, list[index % size]); return; } } } } // Emit a literal using an integer value base on the sem type. e.g. quote it, cast it to blog, etc. static void cg_dummy_test_emit_integer_value(charbuf *output, sem_t col_type, int32_t value) { if (is_numeric(col_type)) { bprintf(output, "%d", value); } else if (is_blob(col_type)) { bprintf(output, "CAST(\'%d\' as blob)", value); } else { bprintf(output, "\'%d\'", value); } } // Emit INSERT statement for a table by using @dummy_seed to generated dummy data // but also info in dummy_test attribute. If column's values are provided in // dummy_test info for the table, it'll be used otherwise @dummy_seed is used to // populated seed value into table. static void cg_dummy_test_populate (charbuf *gen_insert_tables, ast_node *table_ast, int32_t *dummy_value_seed) { Contract(is_ast_create_table_stmt(table_ast)); sem_struct *sptr = table_ast->sem->sptr; CSTR table_name = sptr->struct_name; bool_t add_row; int32_t row_index = -1; symtab_entry *table_entry = symtab_find(dummy_test_infos, table_name); do { row_index++; add_row = 0; CHARBUF_OPEN(names); CHARBUF_OPEN(values); CSTR comma = ""; symtab *col_syms = symtab_new(); // extract column values for insert statement from dummy_test info and emit // the insert statement if (table_entry) { symtab *table = (symtab *)table_entry->val; for (int32_t j = 0; j < table->capacity; j++) { symtab_entry column_entry = table->payload[j]; if (column_entry.sym) { CSTR column_name = column_entry.sym; bytebuf *column_values_entry = (bytebuf *)column_entry.val; ast_node **column_values = (ast_node **)column_values_entry->ptr; int32_t size = column_values_entry->used/sizeof(void **); if (row_index < size) { CHARBUF_OPEN(str_val); cg_dummy_test_column_value(&str_val, column_values[row_index]); bprintf(&values, "%s%s", comma, str_val.ptr); bprintf(&names, "%s%s", comma, column_name); comma = ", "; symtab_add(col_syms, column_name, NULL); add_row = 1; CHARBUF_CLOSE(str_val); } } } } // we make sure that we add at least DUMMY_TEST_INSERT_ROWS rows per table if (row_index < DUMMY_TEST_INSERT_ROWS) { add_row = 1; } if (add_row) { // provide specific values for primary and foreign column to avoid foreign key violation. for (int32_t i = 0; i < sptr->count; i++) { sem_t col_type = sptr->semtypes[i]; CSTR column_name = sptr->names[i]; // We find primary and foreign key column that are missing values in the // insert statement and add those values to avoid sql foreign key violation eror. if (!symtab_find(col_syms, column_name)) { if (is_referenceable_by_foreign_key(table_ast, column_name) || is_foreign_key(col_type)) { CHARBUF_OPEN(str_val); // we do +1 because index value start at zero and we don't want to insert zero as primary key int32_t index_value = row_index + 1; if (is_foreign_key(col_type)) { cg_parent_column_value(&str_val, table_name, column_name, row_index); if (str_val.used <= 1) { // The parent table does not have explicit dummy info on this column. // In this case the parent table key referenced here was created with default value // between 1 and DUMMY_TEST_INSERT_ROWS. We just need to select one of these default // value. cg_dummy_test_emit_integer_value(&str_val, col_type, cg_validate_value_range(index_value)); } } bprintf(&names, "%s%s", comma, column_name); bprintf(&values, "%s", comma); comma = ", "; if (str_val.used > 1) { bprintf(&values, "%s", str_val.ptr); } else { cg_dummy_test_emit_integer_value(&values, col_type, index_value); } CHARBUF_CLOSE(str_val); } } } bprintf(gen_insert_tables, "INSERT OR IGNORE INTO %s(%s) VALUES(%s) @dummy_seed(%d)%s;\n", table_name, names.ptr, values.ptr, (*dummy_value_seed)++, row_index % 2 == 0 ? "" : " @dummy_nullables @dummy_defaults"); } CHARBUF_CLOSE(values); CHARBUF_CLOSE(names); symtab_delete(col_syms); } while (add_row); } // Walk through all triggers and create a dictionnary of triggers per tables. static void init_all_trigger_per_table() { Contract(all_tables_with_triggers == NULL); all_tables_with_triggers = symtab_new(); for (list_item *item = all_triggers_list; item; item = item->next) { EXTRACT_NOTNULL(create_trigger_stmt, item->ast); EXTRACT_NOTNULL(trigger_body_vers, create_trigger_stmt->right); EXTRACT_NOTNULL(trigger_def, trigger_body_vers->left); EXTRACT_NOTNULL(trigger_condition, trigger_def->right); EXTRACT_NOTNULL(trigger_op_target, trigger_condition->right); EXTRACT_NOTNULL(trigger_target_action, trigger_op_target->right); EXTRACT_ANY_NOTNULL(table_name_ast, trigger_target_action->left); EXTRACT_STRING(table_name, table_name_ast); if (create_trigger_stmt->sem->delete_version > 0) { // dummy_test should not emit deleted trigger continue; } symtab_append_bytes(all_tables_with_triggers, table_name, &create_trigger_stmt, sizeof(create_trigger_stmt)); } } static void init_all_indexes_per_table() { Contract(all_tables_with_indexes == NULL); all_tables_with_indexes = symtab_new(); for (list_item *item = all_indices_list; item; item = item->next) { EXTRACT_NOTNULL(create_index_stmt, item->ast); EXTRACT_NOTNULL(create_index_on_list, create_index_stmt->left); EXTRACT_ANY_NOTNULL(table_name_ast, create_index_on_list->right); EXTRACT_STRING(table_name, table_name_ast); if (create_index_stmt->sem->delete_version > 0) { // dummy_test should not emit deleted indexes continue; } symtab_append_bytes(all_tables_with_indexes, table_name, &create_index_stmt, sizeof(create_index_stmt)); } } // Emit create and drop index statement for all indexes on a table. static void cg_emit_index_stmt( CSTR table_name, charbuf *gen_create_indexes, charbuf *gen_drop_indexes, gen_sql_callbacks *callback) { symtab_entry *indexes_entry = symtab_find(all_tables_with_indexes, table_name); bytebuf *buf = indexes_entry ? (bytebuf *)indexes_entry->val : NULL; ast_node **indexes_ast = buf ? (ast_node **)buf->ptr : NULL; int32_t count = buf ? buf->used / sizeof(*indexes_ast) : 0; gen_set_output_buffer(gen_create_indexes); for (int32_t i = 0; i < count; i++) { ast_node *index_ast = indexes_ast[i]; EXTRACT_NOTNULL(create_index_stmt, index_ast); EXTRACT_NOTNULL(create_index_on_list, create_index_stmt->left); EXTRACT_ANY_NOTNULL(index_name_ast, create_index_on_list->left); EXTRACT_STRING(index_name, index_name_ast); gen_statement_with_callbacks(index_ast, callback); bprintf(gen_create_indexes, ";\n"); bprintf(gen_drop_indexes, "DROP INDEX IF EXISTS %s;\n", index_name); } } static CSTR get_table_or_view_name(ast_node *table_or_view) { CSTR table_name = NULL; if (is_ast_create_table_stmt(table_or_view)) { EXTRACT_NOTNULL(create_table_name_flags, table_or_view->left); EXTRACT_NOTNULL(table_flags_attrs, create_table_name_flags->left); EXTRACT_ANY_NOTNULL(name_ast, create_table_name_flags->right); EXTRACT_STRING(name, name_ast); table_name = name; } else { Contract(is_ast_create_view_stmt(table_or_view)); EXTRACT(view_and_attrs, table_or_view->right); EXTRACT(name_and_select, view_and_attrs->left); EXTRACT_ANY_NOTNULL(name_ast, name_and_select->left); EXTRACT_STRING(name, name_ast); table_name = name; } return table_name; } // Emit procedure for dummy_test attribution. This is the entry point that emit // generated code for dummy_test attribution. // This function will generated stored procedures to : // - Create all the tables referenced in the create proc statement. // - Populate data into all the tables referenced in the create proc statement. // - Drop all the tables referenced in the create proc statement. // - Read tables reference in the create proc statement. // The tables are created, populated and drop in an specific order to avoid foreign key violation or table not existing errors. // But also the data populated in the foreign key columns of these tables do not violate the foreign key constraint. static void cg_test_helpers_dummy_test(ast_node *stmt) { Contract(is_ast_create_proc_stmt(stmt)); EXTRACT_STRING(proc_name, stmt->left); CHARBUF_OPEN(create_triggers); CHARBUF_OPEN(drop_triggers); gen_create_triggers = &create_triggers; gen_drop_triggers = &drop_triggers; dummy_test_info info = { .table_current = NULL, .found_tables = NULL, .found_views = NULL }; // First thing we have to do is gather all the tables that are used transitively by the procedure // that needs dummy_test helpers. find_all_table_nodes(&info, stmt); // If the create proc statement does not reference any tables, there is nothing to emit if (info.found_tables == NULL) { CHARBUF_CLOSE(drop_triggers); CHARBUF_CLOSE(create_triggers); return; } // There are some tables, so we've work to do, there are several types of functions emitted // by this helper type, we're going to need them all. int32_t value_seed = 123; gen_sql_callbacks callbacks; init_gen_sql_callbacks(&callbacks); callbacks.if_not_exists_callback = cg_test_helpers_force_if_not_exists; callbacks.mode = gen_mode_no_annotations; CHARBUF_OPEN(gen_create_tables); CHARBUF_OPEN(gen_drop_tables); CHARBUF_OPEN(gen_populate_tables); CHARBUF_OPEN(gen_read_tables); CHARBUF_OPEN(gen_declare_funcs); CHARBUF_OPEN(gen_drop_indexes); // Here we record that we actually emitted some dummy test stuff for this proc, this helps us // decide if we need the test markers in test mode. helper_flags |= DUMMY_TEST; // The found tables list begins in an order that is correct for dropping (i.e. the "leaf" tables/views are first) // do that now... for (list_item *item = info.found_tables; item; item = item->next) { EXTRACT_ANY_NOTNULL(table_or_view, item->ast); Invariant(is_ast_create_table_stmt(table_or_view) || is_ast_create_view_stmt(table_or_view)); CSTR table_name = get_table_or_view_name(table_or_view); bprintf(&gen_drop_tables, "DROP %s IF EXISTS %s;\n", is_ast_create_table_stmt(table_or_view) ? "TABLE" : "VIEW", table_name); } // Reverse the list to get the tables back into a safe-to-declare order that we can loop over // to emit table creation of parent tables before child tables. reverse_list(&info.found_tables); // For each found table we're going to do some table specific things for (list_item *item = info.found_tables; item; item = item->next) { EXTRACT_ANY_NOTNULL(table_or_view, item->ast); ast_node *ast_to_emit = table_or_view; // the virtual table ast in the symbol table points to the table decl part of the virtual table create // we want the whole statement so we have to back up one notch up the tree. bool_t is_virtual_table = table_or_view->parent && is_ast_create_virtual_table_stmt(table_or_view->parent); if (is_virtual_table) { ast_to_emit = table_or_view->parent; } // First thing we need is the CREATE DDL for the item in question, make that now gen_set_output_buffer(&gen_create_tables); gen_statement_with_callbacks(ast_to_emit, &callbacks); bprintf(&gen_create_tables, ";\n"); // Next we need the DDL for any indices that may be on the table, we'll generate // the CREATE for those indices and a DROP for the indices. The CREATE goes with // the table creates. The indices may be dropped seperately so the DROP goes // in its own buffer CSTR table_name = get_table_or_view_name(table_or_view); cg_emit_index_stmt(table_name, &gen_create_tables, &gen_drop_indexes, &callbacks); // Next we generate a fragment to populate data for this table using the current seed value // We don't do this for views or virtual tables if (is_ast_create_table_stmt(table_or_view) && !is_virtual_table) { cg_dummy_test_populate(&gen_populate_tables, table_or_view, &value_seed); } // Finally, there is a helper procedure for each table or view that just reads all that // data out of it. Most tests don't use all of them but it's only test code so size doesn't // matter so much and it's super easy to have them all handy so we aren't picky. bprintf(&gen_read_tables, "\n"); bprintf(&gen_read_tables, "CREATE PROC test_%s_read_%s()\n", proc_name, table_name); bprintf(&gen_read_tables, "BEGIN\n"); bprintf(&gen_read_tables, " SELECT * FROM %s;\n", table_name); bprintf(&gen_read_tables, "END;\n"); } // At this point we're done with all the tables, we're ready to generate the main methods // plus do the rest of the housekeeping // Emit declare functions because they may be needed for schema and query validation // We don't try to guess which functions were used, we just emit the correct declarations for them all. // We could in principle do this one time for the entire translation unit but duplicates don't hurt anyway. gen_set_output_buffer(&gen_declare_funcs); bprintf(&gen_declare_funcs, "\n"); for (list_item *item = all_functions_list; item; item = item->next) { EXTRACT_ANY_NOTNULL(any_func, item->ast); Contract(is_ast_declare_func_stmt(any_func) || is_ast_declare_select_func_stmt(any_func)); if (is_ast_declare_select_func_stmt(any_func)) { gen_one_stmt(any_func); bprintf(&gen_declare_funcs, ";\n"); } } // declare functions bprintf(cg_th_procs, "%s", gen_declare_funcs.ptr); // create tables proc bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_create_tables()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, &gen_create_tables, 2); bprintf(cg_th_procs, "END;\n"); // Create the triggers proc. // // We emit the trigger creation code in its own proc for two reasons: // // 1. If the code is part of the create table proc it might have unwanted // effects on the dummy data populated later. Some dummy data in the table // will likely be altered because of the triggers and the DB will end up in // an unexpected state. Generally the dummy data is considered authoritative // of the desire end state, it isn't transactions to be applied. // // 2. We want to give the engineer control of if/when the triggers are applied. // // We create the create/drop triggers helpers even for procs that don't use any // tables with triggers. Otherwise callsites might have to change when triggers // are added/removed from the schema. if (gen_drop_triggers->used <= 1) { // Similarly, to avoid the procs signature changing based on triggers being // added/removed we use the below snippet to force the procedure to use the // db-using signature, even if no triggers are actually created. bprintf(gen_create_triggers, "IF @rc THEN END IF;\n"); } bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_create_triggers()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, gen_create_triggers, 2); bprintf(cg_th_procs, "END;\n"); // populate tables proc if (gen_populate_tables.used > 1) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_populate_tables()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, &gen_populate_tables, 2); bprintf(cg_th_procs, "END;\n"); } // drop tables proc bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_drop_tables()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, &gen_drop_tables, 2); bprintf(cg_th_procs, "END;\n"); // drop trigger proc if (gen_drop_triggers->used <= 1) { bprintf(gen_drop_triggers, "IF @rc THEN END IF;\n"); } bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_drop_triggers()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, gen_drop_triggers, 2); bprintf(cg_th_procs, "END;\n"); // read tables procedures bprintf(cg_th_procs, "%s", gen_read_tables.ptr); // drop indexes proc if (gen_drop_indexes.used > 1) { bprintf(cg_th_procs, "\n"); bprintf(cg_th_procs, "CREATE PROC test_%s_drop_indexes()\n", proc_name); bprintf(cg_th_procs, "BEGIN\n"); bindent(cg_th_procs, &gen_drop_indexes, 2); bprintf(cg_th_procs, "END;\n"); } CHARBUF_CLOSE(gen_drop_indexes); CHARBUF_CLOSE(gen_declare_funcs); CHARBUF_CLOSE(gen_read_tables); CHARBUF_CLOSE(gen_populate_tables); CHARBUF_CLOSE(gen_drop_tables); CHARBUF_CLOSE(gen_create_tables); CHARBUF_CLOSE(drop_triggers); CHARBUF_CLOSE(create_triggers); } // check whether "value" already exist in "column_values". This is used to avoid // having the same value repeated in a column. It can only happens if the value // explicitely added to dummy_test info match values from @dummy_seed. static bool_t is_column_value_present(bytebuf *column_values, sem_t column_type, ast_node *value) { bool_t exist = 0; ast_node **list = (ast_node **)column_values->ptr; int32_t size = column_values->used / sizeof(ast_node *); for (int32_t i = 0; i < size; i++) { ast_node *l = list[i]; sem_t col_type = core_type_of(column_type); ast_node *r = value; // The numbers get some special treatment because unary minus might be in the node // if it's present we peel it off and compare what's left. Remember all numerics // are represented as positive numbers with possibly a negation operator if needed. // It has to be this way so that 1-5 doesn't parse as 1 and -5 with no operator. if (col_type == SEM_TYPE_LONG_INTEGER || col_type == SEM_TYPE_INTEGER || col_type == SEM_TYPE_REAL || col_type == SEM_TYPE_BOOL) { bool_t minus_l = is_ast_uminus(l); if (minus_l) { Contract(is_ast_num(l->left)); l = l->left; } bool_t minus_r = is_ast_uminus(r); if (minus_r) { Contract(is_ast_num(r->left)); r = r->left; } EXTRACT_NUM_VALUE(lv, l); EXTRACT_NUM_VALUE(rv, r); exist = minus_l == minus_r && !Strcasecmp(lv, rv); } else if (col_type == SEM_TYPE_TEXT) { EXTRACT_STRING(lv, l); EXTRACT_STRING(rv, r); exist = !Strcasecmp(lv, rv); } if (exist) { return true; } } return false; } // Insert the column value from a child table to the parent table to make sure // the row in child table references a row in then parent table when we emit // insert statement for both tables. This function is only called from foreign key columns. // e.g: Suppose you have table "Foo" with column "id" which is a foreign key reference to // "id" in table "Bar". If the user has manually added a value for the column "id" in // the table "Foo" in dummy_test info then this method will add the same value // to column "id" of the table "Bar" into its dummy_test info. static void add_value_to_referenced_table( CSTR table_name, CSTR column_name, sem_t column_type, ast_node *column_value) { // if the data column is "NULL" then it doesn't actually have to go into the parent at all if (is_ast_null(column_value)) { return; } ast_node *referenced_table_ast; CSTR referenced_column; find_parent_column(&referenced_table_ast, &referenced_column, table_name, column_name); CSTR referenced_table_name = referenced_table_ast->sem->sptr->struct_name; symtab *fk_col_syms = symtab_ensure_symtab(dummy_test_infos, referenced_table_name); bytebuf *fk_column_values = symtab_ensure_bytebuf(fk_col_syms, referenced_column); // We want to avoid adding the same value to multiple rows in the same table. // Note this is imperfect: if the FK relationship is multi-columnar then we're going // to have a bug here. e.g. if the FK columns are (a,b) and we have already added // (1,2) to the fk table we could get into trouble when try to add (1,3) because // then "1" will look like it's already there. We live with this limitation // because this is only a test helper... it's entirely optional anyway and if you // really want full control you can always write your own data inserter. if (!is_column_value_present(fk_column_values, column_type, column_value)) { bytebuf_append_var(fk_column_values, column_value); } } // Walk through the dummy_test attributes collecting this information. This // is a set of columns and values which will later be used in the generated // data insertion procedure. This is entirely optional but if you want specific // data to be inserted you can put it in the attribute. static void collect_dummy_test_info( ast_node *_Nullable misc_attr_value_list, void *_Nullable context) { EXTRACT_STRING(autotest_attr_name, misc_attr_value_list->left); if (is_autotest_dummy_test(autotest_attr_name)) { // walkthrough dummy_test tree and retreive the table name then the column name // of the table name and then the column values of the column names. We repeat // it for the next table info. for (ast_node *dummy_attr = misc_attr_value_list->right; dummy_attr; dummy_attr = dummy_attr->right) { bytebuf col_data_buf; bytebuf col_type_buf; bytebuf col_name_buf; bytebuf_open(&col_data_buf); bytebuf_open(&col_type_buf); bytebuf_open(&col_name_buf); // the data attribute looks kind of like this: // @attribute(cql:autotest = ( // .. other auto test attributes // (dummy_test, // (table_name1, (col1, col2), (col1_val1, col2_val1), (col1_val2, col2_val2) ), // (table_name2, (col1, col2), (col1_val1, col2_val1), (col1_val2, col2_val2) ), // ... // ) // .. other auto test attributes // )) // // we're concerned with the dummy_test entries here, they have a very specific format // i.e. first the table then the column names, and then a list of matching columns and values // note that sem.c has already verified the correct shape, see error CQL0277 // collect table name from dummy_test info ast_node *table_list = dummy_attr->left; EXTRACT_STRING(table_name, table_list->left); symtab *col_syms = symtab_ensure_symtab(dummy_test_infos, table_name); // collect column names from dummy_test info ast_node *column_name_list = table_list->right; for (ast_node *list = column_name_list->left; list; list = list->right) { EXTRACT_STRING(column_name, list->left); sem_t col_type = find_column_type(table_name, column_name); bytebuf *column_values = symtab_ensure_bytebuf(col_syms, column_name); // store the column meta data, create space to hold values in databuf bytebuf_append_var(&col_data_buf, column_values); bytebuf_append_var(&col_type_buf, col_type); bytebuf_append_var(&col_name_buf, column_name); } // collect column value from dummy_test info. We can have multiple rows of column value for (ast_node *values_ast = column_name_list->right; values_ast; values_ast = values_ast->right) { int32_t column_index = 0; // collect one row of column value for (ast_node *list = values_ast->left; list; list = list->right) { ast_node *misc_attr_value = list->left; Contract(col_data_buf.used); bytebuf *column_values = ((bytebuf **) col_data_buf.ptr)[column_index]; sem_t column_type = ((sem_t *) col_type_buf.ptr)[column_index]; CSTR column_name = ((CSTR *) col_name_buf.ptr)[column_index]; bytebuf_append_var(column_values, misc_attr_value); column_index++; // If a column value is added to dummy_test info for a foreign key column then // we need to make sure that same column value is also added as a value in the // the referenced table's dummy_test info. // e.g. // create table A(id integer primary key); // create table B(id integer primary key references A(id)); // // If there is sample data provided for B.id then we must also ensure that // the value provided for B.id is also add as a sample row in A with the same // value for id. if (is_foreign_key(column_type)) { add_value_to_referenced_table(table_name, column_name, column_type, misc_attr_value); } } } bytebuf_close(&col_data_buf); bytebuf_close(&col_type_buf); bytebuf_close(&col_name_buf); } } } // This is invoked for every misc attribute on every create proc statement // in this translation unit. We're looking for attributes of the form cql:autotest=(...) // and we ignore anything else. static void test_helpers_find_ast_misc_attr_callback( CSTR _Nullable misc_attr_prefix, CSTR _Nonnull misc_attr_name, ast_node *_Nullable ast_misc_attr_value_list, void *_Nullable context) { ast_node *stmt = (ast_node *)context; Contract(is_ast_create_proc_stmt(stmt)); if (misc_attr_prefix && misc_attr_name && !Strcasecmp(misc_attr_prefix, "cql") && !Strcasecmp(misc_attr_name, "autotest")) { // We're actually using intermediate buffers here only so that // we can test if they were used (non-empty) at the end so that // we can emit the test delimeters if and only if they are needed // these are otherwise going to pass through to gh_th_decls and _procs // as they came in. CHARBUF_OPEN(decls_temp); CHARBUF_OPEN(procs_temp); charbuf *decls_saved = cg_th_decls; charbuf *procs_saved = cg_th_procs; cg_th_decls = &decls_temp; cg_th_procs = &procs_temp; EXTRACT_STRING(proc_name, stmt->left); for (ast_node *list = ast_misc_attr_value_list; list; list = list->right) { ast_node *misc_attr_value = list->left; // We found a nested list which should be nested dummy_test with info // @attribute(cql:autotest=(..., (dummy_test, ...), ...)) if (is_ast_misc_attr_value_list(misc_attr_value)) { collect_dummy_test_info(misc_attr_value, context); cg_test_helpers_dummy_test(stmt); } // we found autotest attribution // @attribute(cql:autotest=(dummy_table, dummy_test, dummy_insert, dummy_select, dummy_result_set)) else { // In principle, any option can be combined with any other but some only make sense for procs with // a result. EXTRACT_STRING(autotest_attr_name, misc_attr_value); if (is_autotest_dummy_test(autotest_attr_name)) { cg_test_helpers_dummy_test(stmt); } // these options are only for procs that return a result set if (has_result_set(stmt) || has_out_stmt_result(stmt) || has_out_union_stmt_result(stmt)) { if (is_autotest_dummy_table(autotest_attr_name)) { helper_flags |= DUMMY_TABLE; cg_test_helpers_dummy_table(proc_name); } else if (is_autotest_dummy_insert(autotest_attr_name)) { helper_flags |= DUMMY_INSERT; cg_test_helpers_dummy_insert(proc_name); } else if (is_autotest_dummy_select(autotest_attr_name)) { helper_flags |= DUMMY_SELECT; cg_test_helpers_dummy_select(proc_name); } else if (is_autotest_dummy_result_set(autotest_attr_name)) { helper_flags |= DUMMY_RESULT_SET; cg_test_helpers_dummy_result_set(proc_name); } } } } if (is_declare_proc_needed()) { // if we emitted one of the helpers above that sets helper_flags it tells us that we // need to emit a declaration for the procedure that had the attribute (i.e. the thing // we are trying to mock). The generated code uses the name of that procedure in a LIKE // clause and it won't otherwise be in our output so we emit a declaration for it here. cg_test_helpers_declare_proc(stmt); } cg_th_decls = decls_saved; cg_th_procs = procs_saved; // generate test delimiters only if needed if (decls_temp.used > 1) { if (options.test) { bprintf(cg_th_decls, "\n-- The statement ending at line %d", stmt->lineno); } bprintf(cg_th_decls, "%s", decls_temp.ptr); } // We always generate a marker in the procs section, because there are cases // where we need to verify that we generated nothing. if (options.test) { bprintf(cg_th_procs, "\n-- The statement ending at line %d", stmt->lineno); if (procs_temp.used == 1) { // this gives us a nice clear message in the output bprintf(cg_th_procs, "\n-- no output generated --\n"); } } bprintf(cg_th_procs, "%s", procs_temp.ptr); CHARBUF_CLOSE(procs_temp); CHARBUF_CLOSE(decls_temp); } } // Having found a create proc statement, we set up to get the attributes on it. // The find_misc_attrs callback will be invoked for every attribute on the procedure. // test_helpers_find_ast_misc_attr_callback() will look for the relevant ones. static void cg_test_helpers_create_proc_stmt(ast_node *stmt, ast_node *misc_attrs) { Contract(is_ast_create_proc_stmt(stmt)); if (misc_attrs) { helper_flags = 0; dummy_test_infos = symtab_new(); find_misc_attrs(misc_attrs, test_helpers_find_ast_misc_attr_callback, stmt); symtab_delete(dummy_test_infos); dummy_test_infos = NULL; } } // Iterate through statement list static void cg_test_helpers_stmt_list(ast_node *head) { Contract(is_ast_stmt_list(head)); init_all_trigger_per_table(); init_all_indexes_per_table(); CHARBUF_OPEN(procs_buf); CHARBUF_OPEN(decls_buf); cg_th_procs = &procs_buf; cg_th_decls = &decls_buf; for (ast_node *ast = head; ast; ast = ast->right) { EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc_attrs, ast); if (is_ast_create_proc_stmt(stmt)) { EXTRACT_STRING(proc_name, stmt->left); cg_test_helpers_create_proc_stmt(stmt, misc_attrs); } } bprintf(cg_th_output, "%s", decls_buf.ptr); bprintf(cg_th_output, "\n"); bprintf(cg_th_output, "%s", procs_buf.ptr); CHARBUF_CLOSE(decls_buf); CHARBUF_CLOSE(procs_buf); symtab_delete(all_tables_with_triggers); all_tables_with_triggers = NULL; symtab_delete(all_tables_with_indexes); all_tables_with_indexes = NULL; } // Force the globals to null state so that they do not look like roots to LeakSanitizer // all of these should have been freed already. This is the final safety net to prevent // non-reporting of leaks. static void cg_test_helpers_reset_globals() { gen_create_triggers = NULL; gen_drop_triggers = NULL; all_tables_with_triggers = NULL; all_tables_with_indexes = NULL; dummy_test_infos = NULL; cg_th_output = NULL; cg_th_decls = NULL; cg_th_procs = NULL; helper_flags = 0; } // Main entry point for test_helpers cql_noexport void cg_test_helpers_main(ast_node *head) { Contract(options.file_names_count == 1); cql_exit_on_semantic_errors(head); exit_on_validating_schema(); cg_test_helpers_reset_globals(); CHARBUF_OPEN(output_buf); cg_th_output = &output_buf; bprintf(cg_th_output, "%s", rt->source_prefix); cg_test_helpers_stmt_list(head); cql_write_file(options.file_names[0], cg_th_output->ptr); CHARBUF_CLOSE(output_buf); cg_test_helpers_reset_globals(); } #endif