sources/cg_json_schema.c (1,776 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_JSON)
// stubs to avoid link errors
cql_noexport void cg_json_schema_main(ast_node *head) {}
#else
// Perform codegen of the various nodes to json schema format
#include "cg_json_schema.h"
#include <stdio.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 "encoders.h"
#include "eval.h"
#include "cg_common.h"
static void cg_fragment_with_params(charbuf *output, CSTR frag, ast_node *ast, gen_func fn);
static void cg_fragment_with_params_raw(charbuf *output, CSTR frag, ast_node *ast, gen_func fn);
static void cg_json_fk_target_options(charbuf *output, ast_node *ast);
static void cg_json_emit_region_info(charbuf *output, ast_node *ast);
static void cg_json_dependencies(charbuf *output, ast_node *ast);
static void cg_json_data_type(charbuf *output, sem_t sem_type, CSTR kind);
// These little helpers are for handling comma seperated lists where you may or may
// not need a comma in various places. The local tracks if there is an item already
// present and you either get ",\n" or just "\n" as needed.
#define BEGIN_LIST bool_t list_start = 1
#define CONTINUE_LIST bool_t list_start = 0
#define COMMA if (!list_start) bprintf(output, ",\n"); else list_start = 0
#define END_LIST if (!list_start) bprintf(output, "\n")
// These are the main output buffers for the various forms of statements we support
// we build these up as we encounter them, redirecting the local 'output' to one of these
static charbuf *queries;
static charbuf *deletes;
static charbuf *inserts;
static charbuf *updates;
static charbuf *general;
static charbuf *general_inserts;
// We use this to track every table we've ever seen and we remember what stored procedures use it
static symtab *tables_to_procs;
// The callback function for dependency analysis gets this structure as the anonymous context
typedef struct json_context {
CSTR cookie;
ast_node *proc_ast;
charbuf *used_tables;
charbuf *used_views;
charbuf *insert_tables;
charbuf *update_tables;
charbuf *delete_tables;
charbuf *from_tables;
charbuf *used_procs;
} json_context;
// magic string to sanity check the context cuz we're paranoid
static char cookie_str[] = "cookie";
// compute the CRC for an arbitrary statement
static llint_t crc_stmt(ast_node *stmt) {
CHARBUF_OPEN(temp);
// Format the text with full annotations, this text isn't going to SQLite but it should capture
// all aspects of the table/view/index/trigger including annotations.
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
gen_set_output_buffer(&temp);
gen_statement_with_callbacks(stmt, &callbacks);
llint_t result = (llint_t)crc_charbuf(&temp);
CHARBUF_CLOSE(temp);
return result;
}
static void add_name_to_output(charbuf* output, CSTR table_name) {
Contract(output);
if (output->used > 1) {
bprintf(output, ", ");
}
bprintf(output, "\"%s\"", table_name);
}
// This is the callback function that tells us a view name was found in the body
// of the stored proc we are currently examining. The void context information
// is how we remember which proc we were processing. For each table we have
// a character buffer. We look it up, create it if not present, and write into it.
// We also write into the buffer for the current proc which came in with the context.
static void cg_found_view(CSTR view_name, ast_node* table_ast, void* pvContext) {
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
Contract(context->used_views);
add_name_to_output(context->used_views, view_name);
}
// This is the callback function that tells us a table name was found in the body
// of the stored proc we are currently examining. The void context information
// is how we remember which proc we were processing. For each table we have
// a character buffer. We look it up, create it if not present, and write into it.
// We also write into the buffer for the current proc which came in with the context.
static void cg_found_table(CSTR table_name, ast_node* table_ast, void* pvContext) {
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
Contract(context->used_tables);
ast_node *proc_ast = context->proc_ast;
if (is_ast_create_proc_stmt(proc_ast)) {
Contract(tables_to_procs);
charbuf* output = symtab_ensure_charbuf(tables_to_procs, table_name);
// Get the proc name and add it to the list for this table
EXTRACT_STRING(proc_name, proc_ast->left);
add_name_to_output(output, proc_name);
}
add_name_to_output(context->used_tables, table_name);
}
static void cg_found_insert(CSTR table_name, ast_node *table_ast, void *pvContext)
{
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
add_name_to_output(context->insert_tables, table_name);
}
static void cg_found_update(CSTR table_name, ast_node *table_ast, void *pvContext)
{
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
add_name_to_output(context->update_tables, table_name);
}
static void cg_found_delete(CSTR table_name, ast_node *table_ast, void *pvContext)
{
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
add_name_to_output(context->delete_tables, table_name);
}
static void cg_found_from(CSTR table_name, ast_node *table_ast, void *pvContext)
{
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
add_name_to_output(context->from_tables, table_name);
}
static void cg_found_proc(CSTR proc_name, ast_node *name_ast, void *pvContext)
{
json_context *context = (json_context *)pvContext;
Contract(context->cookie == cookie_str); // sanity check
add_name_to_output(context->used_procs, proc_name);
}
// When processing generated SQL we get a callback every time a variable appears
// in the output stream. This method records the variable name for use later
// in the _parameters array.
static bool_t cg_json_record_var(struct ast_node *_Nonnull ast, void *_Nullable context, charbuf *_Nonnull output) {
// If this invariant fails that means the code is using cg_fragment when
// cg_fragment_with_params is required (because variables were used).
Invariant(context);
charbuf *var = (charbuf *)context;
bprintf(output, "?");
if (var->used > 1) {
bprintf(var, ", ");
}
bprintf(var, "\"%s\"", ast->sem->name);
return true;
}
static void cg_json_test_details(charbuf *output, ast_node *ast, ast_node *misc_attrs) {
if (options.test) {
bprintf(output, "\nThe statement ending at line %d\n", ast->lineno);
bprintf(output, "\n");
gen_set_output_buffer(output);
if (misc_attrs) {
gen_with_callbacks(misc_attrs, gen_misc_attrs, NULL);
}
gen_with_callbacks(ast, gen_one_stmt, NULL);
bprintf(output, "\n\n");
}
}
// Just extract a name from the AST and emit it
static void cg_json_name(charbuf *output, ast_node *ast) {
EXTRACT_STRING(name, ast);
bprintf(output, "%s", name);
}
// Emit a quoted string into the JSON
static void cg_json_emit_string(charbuf *output, ast_node *ast) {
Invariant(is_strlit(ast));
EXTRACT_STRING(str, ast);
// note str is the lexeme, so it is still quoted and escaped
CHARBUF_OPEN(str1);
CHARBUF_OPEN(str2);
// requote it as a c style literal
cg_decode_string_literal(str, &str1);
cg_encode_json_string_literal(str1.ptr, &str2);
bprintf(output, "%s", str2.ptr);
CHARBUF_CLOSE(str2);
CHARBUF_CLOSE(str1);
}
// Emit out a single miscellaneous attribute value into the current output stream
// We could be processing any kind of entity, we don't care. We're just
// emitting a single value here. The legal values are:
// * a list of nested values
// * an integer literal
// * a double literal
// * a string literal
// * a name
// * null
// String literals have to be unescaped from SQL format and reescaped into C format
static void cg_json_attr_value(charbuf *output, ast_node *ast) {
if (is_ast_misc_attr_value_list(ast)) {
bprintf(output, "[");
for (ast_node *item = ast; item; item = item->right) {
cg_json_attr_value(output, item->left);
if (item->right) {
bprintf(output, ", ");
}
}
bprintf(output, "]");
}
else if (is_ast_str(ast)) {
EXTRACT_STRING(str, ast);
if (is_strlit(ast)) {
cg_json_emit_string(output, ast);
}
else {
bprintf(output, "\"%s\"", str); // an identifier
}
}
else if (is_ast_null(ast)) {
// Null must be all in lowercase to be valid json, so we need a special case
bprintf(output, "null");
}
else {
gen_set_output_buffer(output);
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
callbacks.mode = gen_mode_sql;
callbacks.convert_hex = true; // json doesn't support hex numbers
gen_with_callbacks(ast, gen_root_expr, &callbacks);
}
}
// Emit a single miscellaneous attribute name/value pair
// The name could be of the form foo:bar in which case we emit foo_bar
// The value is any of the legal values handled above in cg_json_attr_value.
static void cg_json_misc_attr(charbuf *output, ast_node *ast) {
Contract(is_ast_misc_attr(ast));
bprintf(output, "{\n");
BEGIN_INDENT(attr, 2);
bprintf(output, "\"name\" : \"");
if (is_ast_dot(ast->left)) {
cg_json_name(output, ast->left->left);
bprintf(output, ":");
cg_json_name(output, ast->left->right);
}
else {
cg_json_name(output, ast->left);
}
bprintf(output, "\",\n\"value\" : ");
if (ast->right) {
cg_json_attr_value(output, ast->right);
}
else {
bprintf(output, "1");
}
END_INDENT(attr);
bprintf(output, "\n}");
}
// Emit a list of attributes for the current entity, it could be any kind of entity.
// Whatever it is we spit out the attributes here in array format.
static void cg_json_misc_attrs(charbuf *output, ast_node *_Nonnull list) {
Contract(is_ast_misc_attrs(list));
bprintf(output, "\"attributes\" : [\n");
BEGIN_INDENT(attr, 2);
BEGIN_LIST;
for (ast_node *item = list; item; item = item->right) {
COMMA;
cg_json_misc_attr(output, item->left);
}
END_LIST;
END_INDENT(attr);
bprintf(output, "]");
}
// The column has its definition and attributes, output for the column goes
// into the direct output and maybe to the deferred outputs. For instance if
// a column in an FK then is FKness is emitted into the fk buffer for later use
// in the fk section.
typedef struct col_info {
// Inputs
ast_node *def;
ast_node *attrs;
// We write to these
charbuf *col_pk;
charbuf *col_uk;
charbuf *col_fk;
} col_info;
static void cg_json_default_value(charbuf *output, ast_node *def) {
if (is_ast_uminus(def)) {
def = def->left;
bprintf(output, "-");
}
cg_json_attr_value(output, def);
}
// Emits the JSON for all ad-hoc migration procs as a list
static void cg_json_ad_hoc_migration_procs(charbuf* output) {
bprintf(output, "\"adHocMigrationProcs\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
for (list_item *item = all_ad_hoc_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_schema_ad_hoc_migration_stmt(ast));
EXTRACT(version_annotation, ast->left);
EXTRACT_OPTION(version, version_annotation->left);
EXTRACT_STRING(name, version_annotation->right);
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(t, 2);
bprintf(output, "\"name\" : \"%s\",\n", name);
bprintf(output, "\"CRC\" : \"%lld\",\n", crc_stmt(ast));
if (misc_attrs) {
cg_json_misc_attrs(output, misc_attrs);
bprintf(output, ",\n");
}
bprintf(output, "\"version\" : %d", version);
END_INDENT(t);
bprintf(output, "\n}");
}
uint32_t count = ad_hoc_recreate_actions->count;
symtab_entry *actions = symtab_copy_sorted_payload(ad_hoc_recreate_actions, default_symtab_comparator);
for (int32_t i = 0; i < count; i++) {
ast_node *ast = (ast_node *)actions[i].val;
EXTRACT_STRING(group, ast->left);
EXTRACT_STRING(proc, ast->right);
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(t, 2);
bprintf(output, "\"name\" : \"%s\",\n", proc);
bprintf(output, "\"CRC\" : \"%lld\",\n", crc_stmt(ast));
if (misc_attrs) {
cg_json_misc_attrs(output, misc_attrs);
bprintf(output, ",\n");
}
bprintf(output, "\"onRecreateOf\" : \"%s\"", group);
END_INDENT(t);
bprintf(output, "\n}");
}
free(actions);
END_LIST;
END_INDENT(list);
bprintf(output, "]");
}
// Emits the name and value for each value in the enumeration
static void cg_json_enum_values(ast_node *enum_values, charbuf *output) {
Contract(is_ast_enum_values(enum_values));
bprintf(output, "\"values\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
while (enum_values) {
EXTRACT_NOTNULL(enum_value, enum_values->left);
EXTRACT_ANY_NOTNULL(enum_name_ast, enum_value->left);
EXTRACT_STRING(enum_name, enum_name_ast);
COMMA;
bprintf(output, "{\n");
bprintf(output, " \"name\" : \"%s\",\n", enum_name);
bprintf(output, " \"value\" : ");
eval_format_number(enum_name_ast->sem->value, output);
bprintf(output, "\n}");
enum_values = enum_values->right;
}
END_LIST;
END_INDENT(list);
bprintf(output, "]\n");
}
// Emits the JSON for all enumerations
static void cg_json_enums(charbuf* output) {
bprintf(output, "\"enums\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
for (list_item *item = all_enums_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_declare_enum_stmt(ast));
EXTRACT_NOTNULL(typed_name, ast->left);
EXTRACT_NOTNULL(enum_values, ast->right);
EXTRACT_ANY(name_ast, typed_name->left);
EXTRACT_STRING(name, name_ast);
EXTRACT_ANY_NOTNULL(type, typed_name->right);
cg_json_test_details(output, ast, NULL);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(t, 2);
bprintf(output, "\"name\" : \"%s\",\n", name);
cg_json_data_type(output, type->sem->sem_type | SEM_TYPE_NOTNULL, NULL);
bprintf(output, ",\n");
cg_json_enum_values(enum_values, output);
END_INDENT(t);
bprintf(output, "}");
}
END_LIST;
END_INDENT(list);
bprintf(output, "]");
}
// emits the type and value for each constant in the constant group
static void cg_json_const_values(ast_node *const_values, charbuf *output) {
Contract(is_ast_const_values(const_values));
bprintf(output, "\"values\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
while (const_values) {
EXTRACT_NOTNULL(const_value, const_values->left);
EXTRACT_ANY_NOTNULL(const_name_ast, const_value->left);
EXTRACT_STRING(const_name, const_name_ast);
EXTRACT_ANY_NOTNULL(const_expr, const_value->right);
COMMA;
bprintf(output, "{\n");
bprintf(output, " \"name\" : \"%s\",\n", const_name);
BEGIN_INDENT(type, 2);
cg_json_data_type(output, const_expr->sem->sem_type, const_expr->sem->kind);
END_INDENT(type);
bprintf(output, ",\n");
bprintf(output, " \"value\" : ");
if (is_strlit(const_expr)) {
cg_json_emit_string(output, const_expr);
}
else {
eval_format_number(const_expr->sem->value, output);
}
bprintf(output, "\n}");
const_values = const_values->right;
}
END_LIST;
END_INDENT(list);
bprintf(output, "]\n");
}
// Emits the JSON for all the global constants
// note that these are in groups for convenience but they are all global
// scope, not like enums.
static void cg_json_constant_groups(charbuf* output) {
bprintf(output, "\"constantGroups\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
for (list_item *item = all_constant_groups_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_declare_const_stmt(ast));
EXTRACT_ANY_NOTNULL(name_ast, ast->left);
EXTRACT_NOTNULL(const_values, ast->right);
EXTRACT_STRING(name, name_ast);
cg_json_test_details(output, ast, NULL);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(t, 2);
bprintf(output, "\"name\" : \"%s\",\n", name);
cg_json_const_values(const_values, output);
END_INDENT(t);
bprintf(output, "}");
}
END_LIST;
END_INDENT(list);
bprintf(output, "]");
}
// Emits the JSON for all the subscription directives (@unsub/@resub)
static void cg_json_subscriptions(charbuf* output) {
bprintf(output, "\"subscriptions\" : [\n");
BEGIN_INDENT(list, 2);
BEGIN_LIST;
for (list_item *item = all_subscriptions_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_schema_unsub_stmt(ast) || is_ast_schema_resub_stmt(ast));
EXTRACT_NOTNULL(version_annotation, ast->left);
EXTRACT_OPTION(vers, version_annotation->left);
EXTRACT_STRING(name, version_annotation->right);
cg_json_test_details(output, ast, NULL);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(t, 2);
bprintf(output, "\"type\" : \"%s\",\n", is_ast_schema_unsub_stmt(ast) ? "unsub" : "resub");
bprintf(output, "\"table\" : \"%s\",\n", name);
bprintf(output, "\"version\" : %d\n", vers);
END_INDENT(t);
bprintf(output, "}");
}
END_LIST;
END_INDENT(list);
bprintf(output, "]");
}
static void cg_migration_proc(ast_node *ast, charbuf *output) {
if (is_ast_dot(ast)) {
EXTRACT_NOTNULL(dot, ast);
EXTRACT_STRING(lhs, dot->left);
EXTRACT_STRING(rhs, dot->right);
bprintf(output,"\"%s:%s\"", lhs, rhs);
}
else {
EXTRACT_STRING(migration_proc_name, ast);
bprintf(output,"\"%s\"", migration_proc_name);
}
}
// Searches for an "ast_create" node from a list and emits the name of the migration
// proc associated with it, if any
static void cg_json_added_migration_proc(charbuf *output, ast_node *list) {
for (ast_node *attr = list; attr; attr = attr->right) {
if (is_ast_create_attr(attr)){
EXTRACT(version_annotation, attr->left);
if (version_annotation && version_annotation->right) {
bprintf(output,",\n\"addedMigrationProc\" : ");
cg_migration_proc(version_annotation->right, output);
}
}
}
}
// Searches for an "ast_delete" node from a list and emits the name of the migration
// proc associated with it, if any
static void cg_json_deleted_migration_proc(charbuf *output, ast_node *list) {
for (ast_node *attr = list; attr; attr = attr->right) {
if (is_ast_delete_attr(attr)){
EXTRACT(version_annotation, attr->left);
if (version_annotation && version_annotation->right) {
bprintf(output,",\n\"deletedMigrationProc\" : ");
cg_migration_proc(version_annotation->right, output);
}
}
}
}
// Crack the semantic info for the column and emit that into the main output
// examine the attributes and emit those as needed.
static void cg_json_col_attrs(charbuf *output, col_info *info) {
// most of the attributes are in the semantic type, we don't have to walk the tree for them
// we do need to check for a default value.
// Note that there are implications associated with this flags and semantic analysis
// makes those conclusions (e.g. pk implies not null)
// We don't want that logic again so we use the semantic type not the raw declaration
ast_node *col = info->def;
sem_t sem_type = col->sem->sem_type;
CSTR name = col->sem->name;
bool_t is_added = col->sem->create_version > 0;
bool_t is_deleted = col->sem->delete_version > 0;
bprintf(output, ",\n\"isAdded\" : %d", is_added);
if (is_added) {
bprintf(output, ",\n\"addedVersion\" : %d", col->sem->create_version);
cg_json_added_migration_proc(output, info->attrs);
}
bprintf(output, ",\n\"isDeleted\" : %d", is_deleted);
if (is_deleted) {
bprintf(output, ",\n\"deletedVersion\" : %d", col->sem->delete_version);
cg_json_deleted_migration_proc(output, info->attrs);
}
if (sem_type & SEM_TYPE_PK) {
bprintf(info->col_pk, "\"%s\"", name);
}
if (sem_type & SEM_TYPE_UK) {
if (info->col_uk->used > 1) {
bprintf(info->col_uk, ",\n");
}
bprintf(info->col_uk, "{\n");
bprintf(info->col_uk, " \"name\" : \"%s_uk\",\n", name);
bprintf(info->col_uk, " \"columns\" : [ \"%s\" ],\n", name);
bprintf(info->col_uk, " \"sortOrders\" : [ \"\" ]\n");
bprintf(info->col_uk, "}");
}
// There could be several foreign keys, we have to walk the list of attributes and gather them all
for (ast_node *attr = info->attrs; attr; attr = attr->right) {
charbuf *saved = output;
output = info->col_fk;
if (is_ast_col_attrs_fk(attr)) {
if (output->used > 1) {
bprintf(output, ",\n");
}
bprintf(output, "{\n");
BEGIN_INDENT(fk, 2)
bprintf(output, "\"columns\" : [ \"%s\" ]", name);
cg_json_fk_target_options(output, attr->left);
END_INDENT(fk);
bprintf(output,"\n}");
}
output = saved;
}
if (sem_type & SEM_TYPE_HAS_DEFAULT) {
bprintf(output, ",\n\"defaultValue\" : ");
cg_json_default_value(output, sem_get_col_default_value(info->attrs));
}
if (sem_type & SEM_TYPE_HAS_COLLATE) {
// find the collate attribute and emit it (there can only be one)
for (ast_node *attr = info->attrs; attr; attr = attr->right) {
if (is_ast_col_attrs_collate(attr)) {
bprintf(output, ",\n\"collate\" : ");
cg_json_attr_value(output, attr->left);
}
}
}
if (sem_type & SEM_TYPE_HAS_CHECK) {
// find the check attribute and emit it (there can only be one)
for (ast_node *attr = info->attrs; attr; attr = attr->right) {
if (is_ast_col_attrs_check(attr)) {
EXTRACT_ANY_NOTNULL(when_expr, attr->left);
cg_fragment_with_params(output, "checkExpr", when_expr, gen_root_expr);
}
}
}
// end with mandatory columns, this makes the json validation with yacc a little easier
bprintf(output, ",\n\"isPrimaryKey\" : %d", !!(sem_type & SEM_TYPE_PK));
bprintf(output, ",\n\"isUniqueKey\" : %d", !!(sem_type & SEM_TYPE_UK));
bprintf(output, ",\n\"isAutoIncrement\" : %d", !!(sem_type & SEM_TYPE_AUTOINCREMENT));
}
// Starting from a semantic type, emit the appropriate JSON type
static void cg_json_data_type(charbuf *output, sem_t sem_type, CSTR kind) {
sem_t core_type = core_type_of(sem_type);
BEGIN_LIST;
COMMA;
bprintf(output, "\"type\" : \"");
switch (core_type) {
case SEM_TYPE_INTEGER: bprintf(output, "integer"); break;
case SEM_TYPE_TEXT: bprintf(output, "text"); break;
case SEM_TYPE_BLOB: bprintf(output, "blob"); break;
case SEM_TYPE_BOOL: bprintf(output, "bool"); break;
case SEM_TYPE_REAL: bprintf(output, "real"); break;
case SEM_TYPE_LONG_INTEGER: bprintf(output, "long"); break;
case SEM_TYPE_OBJECT: bprintf(output, "object"); break;
}
bprintf(output, "\"");
if (kind) {
COMMA;
bprintf(output, "\"kind\" : \"%s\"", kind);
}
bool_t sensitive = !!sensitive_flag(sem_type);
if (sensitive) {
COMMA;
bprintf(output, "\"isSensitive\" : %d", sensitive);
}
COMMA;
bprintf(output, "\"isNotNull\" : %d", !is_nullable(sem_type));
}
// Starting with a column definition, emit the name and type information
// for the column. If there are any miscellaneous attributes emit them as well.
// Finally gather the column attributes like not null etc. and emit those using
// the helper methods above.
static void cg_json_col_def(charbuf *output, col_info *info) {
ast_node *def = info->def;
Contract(is_ast_col_def(def));
EXTRACT_NOTNULL(col_def_type_attrs, def->left);
EXTRACT(misc_attrs, def->right);
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);
bprintf(output, "{\n");
BEGIN_INDENT(col, 2);
bprintf(output, "\"name\" : \"%s\",\n", name);
if (misc_attrs) {
cg_json_misc_attrs(output, misc_attrs);
bprintf(output, ",\n");
}
cg_json_data_type(output, def->sem->sem_type, def->sem->kind);
info->attrs = attrs;
cg_json_col_attrs(output, info);
END_INDENT(col);
bprintf(output, "\n}");
}
// Emits a list of names into a one-line array of quoted names
static void cg_json_name_list(charbuf *output, ast_node *list) {
Contract(is_ast_name_list(list));
for (ast_node *item = list; item; item = item->right) {
bprintf(output, "\"");
cg_json_name(output, item->left);
bprintf(output, "\"");
if (item->right) {
bprintf(output, ", ");
}
}
}
// This is useful for expressions known to have no parameters (e.g. constraint expressions)
// variables are illegal in such things so there can be no binding needed
static void cg_json_vanilla_expr(charbuf *output, ast_node *expr) {
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
callbacks.mode = gen_mode_echo; // we want all the text, unexpanded, so NOT for sqlite output (this is raw echo)
gen_set_output_buffer(output);
gen_with_callbacks(expr, gen_root_expr, &callbacks);
}
// Similar to the above, this is also a list of names but we emit two arrays
// one array for the names and another array for the sort orders specified if any.
// Note unspecified sort orders are emitted as "".
static void cg_json_indexed_columns(charbuf *cols, charbuf *orders, ast_node *list) {
for (ast_node *item = list; item; item = item->right) {
Contract(is_ast_indexed_columns(list));
EXTRACT_NOTNULL(indexed_column, item->left);
bprintf(cols, "\"");
cg_json_vanilla_expr(cols, indexed_column->left);
bprintf(cols, "\"");
if (is_ast_asc(indexed_column->right)) {
bprintf(orders, "\"asc\"");
}
else if (is_ast_desc(indexed_column->right)) {
bprintf(orders, "\"desc\"");
}
else {
bprintf(orders, "\"\"");
}
if (item->right) {
bprintf(cols, ", ");
bprintf(orders, ", ");
}
}
}
// The primary key def is emitted just as an ordinary name list
static void cg_json_pk_def(charbuf *output, ast_node *def) {
Contract(is_ast_pk_def(def));
EXTRACT_NOTNULL(indexed_columns_conflict_clause, def->right);
EXTRACT_NOTNULL(indexed_columns, indexed_columns_conflict_clause->left);
CHARBUF_OPEN(cols);
CHARBUF_OPEN(orders);
cg_json_indexed_columns(&cols, &orders, indexed_columns);
bprintf(output, "\"primaryKey\" : [ %s ]", cols.ptr);
bprintf(output, ",\n\"primaryKeySortOrders\" : [ %s ],\n", orders.ptr);
CHARBUF_CLOSE(orders);
CHARBUF_CLOSE(cols);
}
// This is just a little wrapper to set up the buffer to get the FK
// resolution action emitted without cloning that code. gen_fk_action
// has a different contract than the usual generators (it doesn't take an AST)
// so I can't use the usual fragment helpers.
static void cg_json_action(charbuf *output, int32_t action) {
CHARBUF_OPEN(sql);
gen_set_output_buffer(&sql);
if (!action) {
action = FK_NO_ACTION;
}
gen_fk_action(action);
bprintf(output, "\"%s\"", sql.ptr);
CHARBUF_CLOSE(sql);
}
// Here we generate the update and delete actions plus the isDeferred state
// Everything is sitting pretty in the AST.
static void cg_json_fk_flags(charbuf *output, int32_t flags) {
int32_t action = (flags & FK_ON_UPDATE) >> 4;
bprintf(output, ",\n\"onUpdate\" : ");
cg_json_action(output, action);
action = (flags & FK_ON_DELETE);
bprintf(output, ",\n\"onDelete\" : ");
cg_json_action(output, action);
// in sqlite anything that is not:
// DEFERRABLE INITIALLY DEFERRED is immediate
// See 4.2. Deferred Foreign Key Constraints
bool_t deferred = (flags & FK_DEFERRABLE) && (flags & FK_INITIALLY_DEFERRED);
bprintf(output, ",\n\"isDeferred\" : %d", deferred);
}
// Generates the properties for a foreign key's target and the options
// that means the referencedTable, the columns as well as the flags
// using the helper above.
static void cg_json_fk_target_options(charbuf *output, ast_node *ast) {
Contract(is_ast_fk_target_options(ast));
EXTRACT_NOTNULL(fk_target, ast->left);
EXTRACT_OPTION(flags, ast->right);
EXTRACT_STRING(table_name, fk_target->left);
EXTRACT_NAMED_NOTNULL(ref_list, name_list, fk_target->right);
bprintf(output, ",\n\"referenceTable\" : \"%s\"", table_name);
bprintf(output, ",\n\"referenceColumns\" : [ ");
cg_json_name_list(output, ref_list);
bprintf(output, " ]");
cg_json_fk_flags(output, flags);
}
// A full FK definition consists of the constrained columns
// and the FK target. This takes care of the columns and defers
// to the above for the target (the target is used in other cases too)
static void cg_json_fk_def(charbuf *output, ast_node *def) {
Contract(is_ast_fk_def(def));
EXTRACT_NOTNULL(fk_info, def->right);
EXTRACT_NAMED_NOTNULL(src_list, name_list, fk_info->left);
EXTRACT_NOTNULL(fk_target_options, fk_info->right);
if (def->left) {
EXTRACT_STRING(name, def->left);
bprintf(output, "\"name\" : \"%s\",\n", name);
}
bprintf(output, "\"columns\" : [ ");
cg_json_name_list(output, src_list);
bprintf(output, " ]");
cg_json_fk_target_options(output, fk_target_options);
}
// A unique key definition is just the name of the key and then
// the participating columns.
static void cg_json_unq_def(charbuf *output, ast_node *def) {
Contract(is_ast_unq_def(def));
EXTRACT_NOTNULL(indexed_columns_conflict_clause, def->right);
EXTRACT_NOTNULL(indexed_columns, indexed_columns_conflict_clause->left);
bprintf(output, "{\n");
BEGIN_INDENT(uk, 2);
if (def->left) {
EXTRACT_STRING(name, def->left);
bprintf(output, "\"name\" : \"%s\",\n", name);
}
CHARBUF_OPEN(cols);
CHARBUF_OPEN(orders);
cg_json_indexed_columns(&cols, &orders, indexed_columns);
bprintf(output, "\"columns\" : [ %s ]", cols.ptr);
bprintf(output, ",\n\"sortOrders\" : [ %s ]", orders.ptr);
CHARBUF_CLOSE(orders);
CHARBUF_CLOSE(cols);
END_INDENT(uk);
bprintf(output, "\n}");
}
// A check constraint is just an expression, possibly named
static void cg_json_check_def(charbuf *output, ast_node *def) {
Contract(is_ast_check_def(def));
EXTRACT_ANY_NOTNULL(expr, def->right);
bprintf(output, "{\n");
BEGIN_INDENT(chk, 2);
if (def->left) {
EXTRACT_STRING(name, def->left);
bprintf(output, "\"name\" : \"%s\",\n", name);
}
cg_fragment_with_params_raw(output, "checkExpr", expr, gen_root_expr);
END_INDENT(chk);
bprintf(output, "\n}");
}
// This is the list of "columns and keys" that form a table. In order to
// organize these we make several passes on the column list. We loop once
// for the columns then again for the PKs, then the FK, and finally UK.
// Note that in each case there is a chance that columns will contribute to
// the contents with keys that are defined directly on the column.
// That's ok, those are just buffered up and emitted with each section.
// All this several passes business just results in for sure all the column direct
// stuff comes before non column related stuff in each section.
static void cg_json_col_key_list(charbuf *output, ast_node *list) {
Contract(is_ast_col_key_list(list));
CHARBUF_OPEN(col_pk);
CHARBUF_OPEN(col_uk);
CHARBUF_OPEN(col_fk);
col_info info;
info.col_pk = &col_pk;
info.col_uk = &col_uk;
info.col_fk = &col_fk;
{
bprintf(output, "\"columns\" : [\n");
BEGIN_INDENT(cols, 2);
BEGIN_LIST;
for (ast_node *item = list; item; item = item->right) {
EXTRACT_ANY_NOTNULL(def, item->left);
if (is_ast_col_def(def)) {
COMMA;
info.def = def;
info.attrs = NULL;
cg_json_col_def(output, &info);
}
}
END_LIST;
END_INDENT(cols);
bprintf(output, "],\n");
}
ast_node *pk_def = NULL;
{
if (col_pk.used > 1) {
bprintf(output, "\"primaryKey\" : [ %s ],\n", col_pk.ptr);
bprintf(output, "\"primaryKeySortOrders\" : [ \"\" ],\n");
}
else {
for (ast_node *item = list; item; item = item->right) {
EXTRACT_ANY_NOTNULL(def, item->left);
if (is_ast_pk_def(def)) {
cg_json_pk_def(output, def);
pk_def = def;
}
}
if (!pk_def) {
bprintf(output, "\"primaryKey\" : [ ],\n");
bprintf(output, "\"primaryKeySortOrders\" : [ ],\n");
}
}
}
if (pk_def && pk_def->left) {
EXTRACT_STRING(pk_name, pk_def->left);
bprintf(output, "\"primaryKeyName\" : \"%s\",\n", pk_name);
}
{
bprintf(output, "\"foreignKeys\" : [\n");
BEGIN_INDENT(fks, 2);
BEGIN_LIST;
if (col_fk.used > 1) {
COMMA;
bprintf(output, "%s", col_fk.ptr);
}
for (ast_node *item = list; item; item = item->right) {
EXTRACT_ANY_NOTNULL(def, item->left);
if (is_ast_fk_def(def)) {
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(fk, 2);
cg_json_fk_def(output, def);
END_INDENT(fk);
bprintf(output, "\n}");
}
}
END_LIST;
END_INDENT(fks);
bprintf(output, "],\n");
}
{
bprintf(output, "\"uniqueKeys\" : [\n");
BEGIN_INDENT(uk, 2);
BEGIN_LIST;
if (col_uk.used > 1) {
COMMA;
bprintf(output, "%s", col_uk.ptr);
}
for (ast_node *item = list; item; item = item->right) {
EXTRACT_ANY_NOTNULL(def, item->left);
if (is_ast_unq_def(def)) {
COMMA;
cg_json_unq_def(output, def);
}
}
END_LIST;
END_INDENT(uk);
bprintf(output, "],\n");
}
{
bprintf(output, "\"checkExpressions\" : [\n");
BEGIN_INDENT(chk, 2);
BEGIN_LIST;
for (ast_node *item = list; item; item = item->right) {
EXTRACT_ANY_NOTNULL(def, item->left);
if (is_ast_check_def(def)) {
COMMA;
cg_json_check_def(output, def);
}
}
END_LIST;
END_INDENT(chk);
bprintf(output, "]");
}
CHARBUF_CLOSE(col_fk);
CHARBUF_CLOSE(col_uk);
CHARBUF_CLOSE(col_pk);
}
// Here we walk all the indices, extract out the key info for each index and
// emit it. The indices have a few flags plus columns and a sort order for
// each column.
static void cg_json_indices(charbuf *output) {
bprintf(output, "\"indices\" : [\n");
BEGIN_INDENT(indices, 2);
BEGIN_LIST;
for (list_item *item = all_indices_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_create_index_stmt(ast));
EXTRACT_NOTNULL(create_index_on_list, ast->left);
EXTRACT_NOTNULL(flags_names_attrs, ast->right);
EXTRACT_NOTNULL(connector, flags_names_attrs->right);
EXTRACT_NOTNULL(index_names_and_attrs, connector->left);
EXTRACT_OPTION(flags, flags_names_attrs->left);
EXTRACT_NOTNULL(indexed_columns, index_names_and_attrs->left);
EXTRACT(opt_where, index_names_and_attrs->right);
EXTRACT_ANY_NOTNULL(index_name_ast, create_index_on_list->left);
EXTRACT_STRING(index_name, index_name_ast);
EXTRACT_ANY_NOTNULL(table_name_ast, create_index_on_list->right);
EXTRACT_STRING(table_name, table_name_ast);
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(index, 2);
bool_t is_deleted = ast->sem->delete_version > 0;
bprintf(output, "\"name\" : \"%s\"", index_name);
bprintf(output, ",\n\"CRC\" : \"%lld\"", crc_stmt(ast));
bprintf(output, ",\n\"table\" : \"%s\"", table_name);
bprintf(output, ",\n\"isUnique\" : %d", !!(flags & INDEX_UNIQUE));
bprintf(output, ",\n\"ifNotExists\" : %d", !!(flags & INDEX_IFNE));
bprintf(output, ",\n\"isDeleted\" : %d", is_deleted);
if (is_deleted) {
bprintf(output, ",\n\"deletedVersion\" : %d", ast->sem->delete_version);
}
if (ast->sem->region) {
cg_json_emit_region_info(output, ast);
}
if (opt_where) {
bprintf(output, ",\n\"where\" : \"");
cg_json_vanilla_expr(output, opt_where->left);
bprintf(output, "\"");
}
if (misc_attrs) {
bprintf(output, ",\n");
cg_json_misc_attrs(output, misc_attrs);
}
CHARBUF_OPEN(cols);
CHARBUF_OPEN(orders);
cg_json_indexed_columns(&cols, &orders, indexed_columns);
bprintf(output, ",\n\"columns\" : [ %s ]", cols.ptr);
bprintf(output, ",\n\"sortOrders\" : [ %s ]", orders.ptr);
CHARBUF_CLOSE(orders);
CHARBUF_CLOSE(cols);
END_INDENT(index);
bprintf(output, "\n}");
}
END_INDENT(indices);
END_LIST;
bprintf(output, "]");
}
static void cg_json_opt_bool(charbuf *output, int32_t flag, CSTR desc) {
if (flag) {
bprintf(output, ",\n\"%s\" : 1", desc);
}
}
// Here we walk all the triggers, we extract the essential flags from
// the trigger statement and emit those into the metadata as well. The main
// body is emitted verbatim.
static void cg_json_triggers(charbuf *output) {
bprintf(output, "\"triggers\" : [\n");
BEGIN_INDENT(indices, 2);
BEGIN_LIST;
for (list_item *item = all_triggers_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_create_trigger_stmt(ast));
EXTRACT_OPTION(flags, ast->left);
EXTRACT_NOTNULL(trigger_body_vers, ast->right);
EXTRACT_NOTNULL(trigger_def, trigger_body_vers->left);
EXTRACT_STRING(trigger_name, trigger_def->left);
EXTRACT_NOTNULL(trigger_condition, trigger_def->right);
EXTRACT_OPTION(cond_flags, trigger_condition->left);
flags |= cond_flags;
EXTRACT_NOTNULL(trigger_op_target, trigger_condition->right);
EXTRACT_NOTNULL(trigger_operation, trigger_op_target->left);
EXTRACT_OPTION(op_flags, trigger_operation->left);
EXTRACT(name_list, trigger_operation->right);
flags |= op_flags;
EXTRACT_NOTNULL(trigger_target_action, trigger_op_target->right);
EXTRACT_ANY_NOTNULL(table_name_ast, trigger_target_action->left);
EXTRACT_NOTNULL(trigger_action, trigger_target_action->right);
EXTRACT_OPTION(action_flags, trigger_action->left);
flags |= action_flags;
EXTRACT_NOTNULL(trigger_when_stmts, trigger_action->right);
EXTRACT_ANY(when_expr, trigger_when_stmts->left);
EXTRACT_NOTNULL(stmt_list, trigger_when_stmts->right);
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
// use the canonical name (which may be case-sensitively different)
CSTR table_name = table_name_ast->sem->sptr->struct_name;
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(trigger, 2);
bool_t is_deleted = ast->sem->delete_version > 0;
bprintf(output, "\"name\" : \"%s\"", trigger_name);
bprintf(output, ",\n\"CRC\" : \"%lld\"", crc_stmt(ast));
bprintf(output, ",\n\"target\" : \"%s\"", table_name);
bprintf(output, ",\n\"isTemp\" : %d", !!(flags & TRIGGER_IS_TEMP));
bprintf(output, ",\n\"ifNotExists\" : %d", !!(flags & TRIGGER_IF_NOT_EXISTS));
bprintf(output, ",\n\"isDeleted\" : %d", is_deleted);
if (is_deleted) {
bprintf(output, ",\n\"deletedVersion\" : %d", ast->sem->delete_version);
}
// only one of these
cg_json_opt_bool(output, (flags & TRIGGER_BEFORE), "isBeforeTrigger");
cg_json_opt_bool(output, (flags & TRIGGER_AFTER), "isAfterTrigger");
cg_json_opt_bool(output, (flags & TRIGGER_INSTEAD_OF), "isInsteadOfTrigger");
// only one of these
cg_json_opt_bool(output, (flags & TRIGGER_DELETE), "isDeleteTrigger");
cg_json_opt_bool(output, (flags & TRIGGER_INSERT), "isInsertTrigger");
cg_json_opt_bool(output, (flags & TRIGGER_UPDATE), "isUpdateTrigger");
cg_json_opt_bool(output, (flags & TRIGGER_FOR_EACH_ROW), "forEachRow");
if (when_expr) {
cg_fragment_with_params(output, "whenExpr", when_expr, gen_root_expr);
}
cg_fragment_with_params(output, "statement", ast, gen_one_stmt);
if (ast->sem->region) {
cg_json_emit_region_info(output, ast);
}
if (misc_attrs) {
bprintf(output, ",\n");
cg_json_misc_attrs(output, misc_attrs);
}
cg_json_dependencies(output, ast);
END_INDENT(trigger);
bprintf(output, "\n}");
}
END_INDENT(indices);
END_LIST;
bprintf(output, "]");
}
static void cg_json_region_deps(charbuf *output, CSTR sym) {
// Every name we encounter has already been validated!
ast_node *region = find_region(sym);
Invariant(region);
bprintf(output, "\"using\" : [ ", sym);
EXTRACT(region_list, region->right);
for (ast_node *item = region_list; item; item = item->right) {
Contract(is_ast_region_list(item));
EXTRACT_NOTNULL(region_spec, item->left);
EXTRACT_STRING(item_name, region_spec->left);
bprintf(output, "\"%s\"", item_name);
if (item->right) {
bprintf(output, ", ");
}
}
bprintf(output, " ]", sym);
bprintf(output, ",\n\"usingPrivately\" : [ ", sym);
for (ast_node *item = region_list; item; item = item->right) {
Contract(is_ast_region_list(item));
EXTRACT_NOTNULL(region_spec, item->left);
EXTRACT_OPTION(type, region_spec->right);
bprintf(output, "%d", type == PRIVATE_REGION);
if (item->right) {
bprintf(output, ", ");
}
}
bprintf(output, " ]", sym);
}
// To compute the deployed_in_region we only need to know its definiton
// * a normal object (not a region) indicates the region it is in by filling
// in the region of its semantic node
// * a region indicates the deployment region it is in by filling in
// the region of its semantic node
// * therefore to go from an object to its deployed region you first
// get the region the object is in and then get the region that region is in
// * note that deployable regions do not necessarily cover everything so
// if the object is in a region that is not yet in an deployable region
// it's marked as an orphan.
static CSTR get_deployed_in_region(ast_node *ast) {
CSTR deployedInRegion = "(orphan)";
// if we are in no region at all, we're an orphan
if (ast->sem->region) {
// this is the region we are in, look it up
ast_node *reg = find_region(ast->sem->region);
// the region of that region is our deployment region
if (reg->sem->region) {
deployedInRegion = reg->sem->region;
}
}
return deployedInRegion;
}
// Emit the region info and the deployment region info as needed
static void cg_json_emit_region_info(charbuf *output, ast_node *ast) {
bprintf(output, ",\n\"region\" : \"%s\"", ast->sem->region);
bprintf(output, ",\n\"deployedInRegion\" : \"%s\"", get_deployed_in_region(ast));
}
// Here we walk all the regions, we get the dependency information for
// that region and emit it.
//
static void cg_json_regions(charbuf *output) {
bprintf(output, "\"regions\" : [\n");
BEGIN_INDENT(regout, 2);
BEGIN_LIST;
symtab_entry *regs = symtab_copy_sorted_payload(schema_regions, default_symtab_comparator);
for (list_item *item = all_regions_list; item; item = item->next) {
ast_node *ast = item->ast;
EXTRACT_STRING(name, ast->left);
cg_json_test_details(output, ast, NULL);
COMMA;
bprintf(output, "{\n\"name\" : \"%s\",\n", name);
CSTR deployedInRegion = get_deployed_in_region(ast);
bprintf(output, "\"isDeployableRoot\" : %d,\n", !!(ast->sem->sem_type & SEM_TYPE_DEPLOYABLE));
bprintf(output, "\"deployedInRegion\" : \"%s\",\n", deployedInRegion);
cg_json_region_deps(output, name);
bprintf(output, "\n}");
}
END_LIST;
END_INDENT(regout);
bprintf(output, "]");
free(regs);
}
// Emit the result columns in the select list -- their names and types.
// This is the projection of the select.
static void cg_json_projection(charbuf *output, ast_node *ast) {
Contract(ast);
Contract(ast->sem);
sem_struct *sptr = ast->sem->sptr;
bprintf(output, ",\n\"projection\" : [\n");
BEGIN_INDENT(proj, 2);
BEGIN_LIST;
for (int32_t i = 0; i < sptr->count; i++) {
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(type, 2);
bprintf(output, "\"name\" : \"%s\",\n", sptr->names[i]);
cg_json_data_type(output, sptr->semtypes[i], sptr->kinds[i]);
END_INDENT(type);
bprintf(output, "\n}");
}
END_LIST;
END_INDENT(proj);
bprintf(output, "]");
}
// The set of views look rather like the query section in as much as
// they are in fact nothing more than named select statements. However
// the output here is somewhat simplified. We only emit the whole select
// statement and any binding args, we don't also emit all the pieces of the select.
static void cg_json_views(charbuf *output) {
bprintf(output, "\"views\" : [\n");
BEGIN_INDENT(views, 2);
int32_t i = 0;
for (list_item *item = all_views_list; item; item = item->next) {
ast_node *ast = item->ast;
Invariant(is_ast_create_view_stmt(ast));
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
EXTRACT_OPTION(flags, ast->left);
EXTRACT(view_and_attrs, ast->right);
EXTRACT(name_and_select, view_and_attrs->left);
EXTRACT_ANY_NOTNULL(select_stmt, name_and_select->right);
EXTRACT_ANY_NOTNULL(name_ast, name_and_select->left);
EXTRACT_STRING(name, name_ast);
if (i > 0) {
bprintf(output, ",\n");
}
bprintf(output, "{\n");
bool_t is_deleted = ast->sem->delete_version > 0;
BEGIN_INDENT(view, 2);
bprintf(output, "\"name\" : \"%s\"", name);
bprintf(output, ",\n\"CRC\" : \"%lld\"", crc_stmt(ast));
bprintf(output, ",\n\"isTemp\" : %d", !!(flags & VIEW_IS_TEMP));
bprintf(output, ",\n\"isDeleted\" : %d", is_deleted);
if (is_deleted) {
bprintf(output, ",\n\"deletedVersion\" : %d", ast->sem->delete_version);
cg_json_deleted_migration_proc(output, view_and_attrs);
}
if (ast->sem->region) {
cg_json_emit_region_info(output, ast);
}
if (misc_attrs) {
bprintf(output, ",\n");
cg_json_misc_attrs(output, misc_attrs);
}
cg_json_projection(output, select_stmt);
cg_fragment_with_params(output, "select", select_stmt, gen_one_stmt);
cg_json_dependencies(output, ast);
END_INDENT(view);
bprintf(output, "\n}\n");
i++;
}
END_INDENT(views);
bprintf(output, "]");
}
static void cg_json_table_indices(list_item *head, charbuf *output) {
bprintf(output, "\"indices\" : [ ");
bool_t needs_comma = 0;
for (list_item *item = head; item; item = item->next) {
ast_node *ast = item->ast;
// don't include deleted indices
if (ast->sem->delete_version > 0) {
continue;
}
Invariant(is_ast_create_index_stmt(ast));
EXTRACT_NOTNULL(create_index_on_list, ast->left);
EXTRACT_ANY_NOTNULL(index_name_ast, create_index_on_list->left);
EXTRACT_STRING(index_name, index_name_ast);
if (needs_comma) {
bprintf(output, ", ");
}
bprintf(output, "\"%s\"", index_name);
needs_comma = 1;
}
bprintf(output, " ]");
}
// The table output is the tables name, the assorted flags, and misc attributes
// the rest of the table output is produced by walking the column and key list
// using the helper above.
static void cg_json_table(charbuf *output, ast_node *ast) {
Invariant(is_ast_create_table_stmt(ast));
EXTRACT_NOTNULL(create_table_name_flags, ast->left);
EXTRACT_NOTNULL(table_flags_attrs, create_table_name_flags->left);
EXTRACT_OPTION(flags, table_flags_attrs->left);
EXTRACT_STRING(name, create_table_name_flags->right);
EXTRACT_ANY_NOTNULL(col_key_list, ast->right);
int32_t temp = flags & TABLE_IS_TEMP;
int32_t if_not_exist = flags & TABLE_IF_NOT_EXISTS;
int32_t no_rowid = flags & TABLE_IS_NO_ROWID;
ast_node *misc_attrs = NULL;
ast_node *attr_target = ast->parent;
if (is_virtual_ast(ast)) {
// for virtual tables, we have to go up past the virtual table node to get the attributes
attr_target = attr_target->parent;
}
if (is_ast_stmt_and_attr(attr_target)) {
EXTRACT_STMT_AND_MISC_ATTRS(stmt, misc, attr_target->parent);
misc_attrs = misc;
}
cg_json_test_details(output, ast, misc_attrs);
bprintf(output, "{\n");
BEGIN_INDENT(table, 2);
bool_t is_added = ast->sem->create_version > 0;
bool_t is_deleted = ast->sem->delete_version > 0;
bprintf(output, "\"name\" : \"%s\"", name);
bprintf(output, ",\n\"CRC\" : \"%lld\"", crc_stmt(ast));
bprintf(output, ",\n\"isTemp\" : %d", !!temp);
bprintf(output, ",\n\"ifNotExists\" : %d", !!if_not_exist);
bprintf(output, ",\n\"withoutRowid\" : %d", !!no_rowid);
bprintf(output, ",\n\"isAdded\" : %d", is_added);
if (is_added) {
bprintf(output, ",\n\"addedVersion\" : %d", ast->sem->create_version);
cg_json_added_migration_proc(output, table_flags_attrs);
}
bprintf(output, ",\n\"isDeleted\" : %d", is_deleted);
if (is_deleted) {
bprintf(output, ",\n\"deletedVersion\" : %d", ast->sem->delete_version);
cg_json_deleted_migration_proc(output, table_flags_attrs);
}
bprintf(output, ",\n\"isRecreated\": %d", ast->sem->recreate);
if (ast->sem->recreate_group_name) {
bprintf(output, ",\n\"recreateGroupName\" : \"%s\"", ast->sem->recreate_group_name);
}
if (ast->sem->unsub_version > ast->sem->resub_version) {
bprintf(output, ",\n\"unsubscribedVersion\" : %d", ast->sem->unsub_version);
}
if (ast->sem->resub_version > ast->sem->unsub_version) {
bprintf(output, ",\n\"resubscribedVersion\" : %d", ast->sem->resub_version);
}
if (ast->sem->region) {
cg_json_emit_region_info(output, ast);
}
if (is_virtual_ast(ast)) {
bprintf(output, ",\n\"isVirtual\" : 1");
bprintf(output, ",\n\"isEponymous\" : %d", !!(flags & VTAB_IS_EPONYMOUS));
EXTRACT_NOTNULL(create_virtual_table_stmt, ast->parent);
EXTRACT_NOTNULL(module_info, create_virtual_table_stmt->left);
EXTRACT_STRING(module_name, module_info->left);
EXTRACT_ANY(module_args, module_info->right);
bprintf(output, ",\n\"module\" : \"%s\"", module_name);
if (module_args) {
bprintf(output, ",\n\"moduleArgs\" : ");
if (is_ast_following(module_args)) {
CHARBUF_OPEN(sql);
gen_set_output_buffer(&sql);
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
gen_with_callbacks(col_key_list, gen_col_key_list, &callbacks);
cg_encode_json_string_literal(sql.ptr, output);
CHARBUF_CLOSE(sql);
}
else {
CHARBUF_OPEN(sql);
gen_set_output_buffer(&sql);
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
gen_with_callbacks(module_args, gen_misc_attr_value_list, &callbacks);
cg_encode_json_string_literal(sql.ptr, output);
CHARBUF_CLOSE(sql);
}
}
}
CONTINUE_LIST;
if (ast->sem->index_list) {
COMMA;
cg_json_table_indices(ast->sem->index_list, output);
}
if (misc_attrs) {
COMMA;
cg_json_misc_attrs(output, misc_attrs);
}
COMMA;
cg_json_col_key_list(output, col_key_list);
END_INDENT(table);
END_LIST;
bprintf(output, "}");
}
// The tables section is simply an array of table entries under the tables key
static void cg_json_tables(charbuf *output) {
bprintf(output, "\"tables\" : [\n");
BEGIN_INDENT(tables, 2);
BEGIN_LIST;
for (list_item *item = all_tables_list; item; item = item->next) {
ast_node *ast = item->ast;
if (is_virtual_ast(ast)) {
continue;
}
COMMA;
cg_json_table(output, ast);
}
END_INDENT(tables);
END_LIST;
bprintf(output, "]");
}
// The tables section is simply an array of table entries under the tables key
static void cg_json_virtual_tables(charbuf *output) {
bprintf(output, "\"virtualTables\" : [\n");
BEGIN_INDENT(tables, 2);
BEGIN_LIST;
for (list_item *item = all_tables_list; item; item = item->next) {
ast_node *ast = item->ast;
if (!is_virtual_ast(ast)) {
continue;
}
COMMA;
cg_json_table(output, ast);
}
END_INDENT(tables);
END_LIST;
bprintf(output, "]");
}
// This helper emits one parameter for a single stored proc. Each will be
// used as the legal arguments to the statement we are binding. If any of
// the parameters are of the 'out' flavor then this proc is "complex"
// so we simply return false and let it fall into the general bucket.
static bool_t cg_json_param(charbuf *output, ast_node *ast, CSTR *infos) {
Contract(is_ast_param(ast));
EXTRACT_ANY(opt_inout, ast->left);
EXTRACT_NOTNULL(param_detail, ast->right);
EXTRACT_STRING(name, param_detail->left);
bool_t simple = 1;
bprintf(output, "{\n");
BEGIN_INDENT(type, 2);
if (is_ast_inout(opt_inout)) {
bprintf(output, "\"binding\" : \"inout\",\n", name);
simple = 0;
}
else if (is_ast_out(opt_inout)) {
bprintf(output, "\"binding\" : \"out\",\n", name);
simple = 0;
}
bprintf(output, "\"name\" : \"%s\",\n", name);
CSTR base_name = infos[0];
CSTR shape_name = infos[1];
CSTR shape_type = infos[2];
if (shape_name[0]) {
// this is an expansion of the form shape_name LIKE shape_type
// the formal arg will have a name like "shape_name_base_name" (underscore between the parts)
bprintf(output, "\"argOrigin\" : \"%s %s %s\",\n", shape_name, shape_type, base_name);
}
else if (shape_type[0]) {
// this is an expansion of the form LIKE shape_type
// the formal arg will have a name like "base_name_" (trailing underscore)
bprintf(output, "\"argOrigin\" : \"%s %s\",\n", shape_type, base_name);
}
else {
// this is a normal arg, it was not auto-expanded from anything
// the formal arg will have the name "base_name"
bprintf(output, "\"argOrigin\" : \"%s\",\n", base_name);
}
cg_json_data_type(output, ast->sem->sem_type, ast->sem->kind);
END_INDENT(type);
bprintf(output, "\n}");
return simple;
}
// Here we walk all the parameters of a stored proc and process each in turn.
// If any parameter is not valid, the entire proc becomes not valid.
static bool_t cg_json_params(charbuf *output, ast_node *ast, CSTR *infos) {
bool_t simple = 1;
BEGIN_LIST;
while (ast) {
Contract(is_ast_params(ast));
EXTRACT_NOTNULL(param, ast->left);
COMMA;
simple &= cg_json_param(output, param, infos);
ast = ast->right;
// There are 3 strings per arg, one each for the shape name, shape type, and base name
// these desribe how automatically generated arguments were created.
infos += 3;
}
END_LIST;
return simple;
}
static bool_t found_shared_fragment;
// simply record the factthat we found a shared fragment
static bool_t cg_json_call_in_cte(ast_node *cte_body, void *context, charbuf *buffer) {
found_shared_fragment = true;
return false;
}
// Use the indicated generation function to create a SQL fragment. The fragment
// may have parameters. They are captured and emitted as an array.
static void cg_fragment_with_params_raw(charbuf *output, CSTR frag, ast_node *ast, gen_func fn) {
CHARBUF_OPEN(sql);
CHARBUF_OPEN(vars);
gen_set_output_buffer(&sql);
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
callbacks.variables_callback = cg_json_record_var;
callbacks.variables_context = &vars;
callbacks.star_callback = cg_expand_star;
callbacks.cte_proc_callback = cg_json_call_in_cte;
found_shared_fragment = false;
bprintf(output, "\"%s\" : ", frag);
gen_with_callbacks(ast, fn, &callbacks);
cg_pretty_quote_plaintext(sql.ptr, output, PRETTY_QUOTE_JSON | PRETTY_QUOTE_SINGLE_LINE);
bprintf(output, ",\n\"%sArgs\" : [ %s ]", frag, vars.ptr);
CHARBUF_CLOSE(vars);
CHARBUF_CLOSE(sql);
}
// Same as the above, but the most common case requires continuing a list
// so this helper does that.
static void cg_fragment_with_params(charbuf *output, CSTR frag, ast_node *ast, gen_func fn)
{
bprintf(output, ",\n");
cg_fragment_with_params_raw(output, frag, ast, fn);
}
// Use the indicated generation function to create a SQL fragment. The fragment
// may not have parameters. This is not suitable for use where expressions
// will be present.
static void cg_fragment(charbuf *output, CSTR frag, ast_node *ast, gen_func fn) {
CHARBUF_OPEN(sql);
gen_set_output_buffer(&sql);
gen_sql_callbacks callbacks;
init_gen_sql_callbacks(&callbacks);
callbacks.variables_callback = cg_json_record_var;
callbacks.variables_context = NULL; // forces invariant violation if any variables
callbacks.star_callback = cg_expand_star;
bprintf(output, ",\n\"%s\" : ", frag);
gen_with_callbacks(ast, fn, &callbacks);
cg_pretty_quote_plaintext(sql.ptr, output, PRETTY_QUOTE_JSON | PRETTY_QUOTE_SINGLE_LINE);
CHARBUF_CLOSE(sql);
}
// The select statement is emitted in two ways. First we emit a fragment for the
// whole statement and its arguments. This is really the easiest way to use the data here.
// Also we emit sub-fragments for each top level piece. The helpers above do most of that
// work. Here we just trigger:
// * the big fragment for everything
// * the select list
// * the expression list and the optional (and highly important) other clauses
static void cg_json_select_stmt(charbuf *output, ast_node *ast) {
Contract(is_select_stmt(ast));
cg_fragment_with_params(output, "statement", ast, gen_one_stmt);
}
// Here we emit the following bits of information
// * the table we are inserting into
// * the insert type (INSERT, INSERT OR REPLACE etc)
// * the insert columns (the ones we are specifying)
// * a fragment for the entire statement with all the args
// * [optional] a fragment for each inserted value with its args
static void cg_json_insert_stmt(charbuf *output, ast_node *ast, bool_t emit_values) {
// Both insert types have been unified in the AST
Contract(is_insert_stmt(ast));
ast_node *insert_stmt = ast;
// extract the insert part it may be behind the WITH clause and it may be the insert part of an upsert
if (is_ast_with_insert_stmt(ast)) {
insert_stmt = ast->right;
}
else if (is_ast_upsert_stmt(ast)) {
insert_stmt = ast->left;
}
else if (is_ast_with_upsert_stmt(ast)) {
insert_stmt = ast->right->left;
}
Contract(is_ast_insert_stmt(insert_stmt));
EXTRACT_ANY_NOTNULL(insert_type, insert_stmt->left);
EXTRACT_NOTNULL(name_columns_values, insert_stmt->right);
EXTRACT_ANY_NOTNULL(name_ast, name_columns_values->left)
EXTRACT_NOTNULL(columns_values, name_columns_values->right);
EXTRACT_NOTNULL(column_spec, columns_values->left);
EXTRACT_ANY(columns_values_right, columns_values->right);
EXTRACT(insert_dummy_spec, insert_type->left);
EXTRACT(name_list, column_spec->left);
// use the canonical name (which may be case-sensitively different)
CSTR name = name_ast->sem->sptr->struct_name;
bprintf(output, ",\n\"table\" : \"%s\"", name);
cg_fragment_with_params(output, "statement", ast, gen_one_stmt);
cg_fragment(output, "statementType", insert_type, gen_insert_type);
bprintf(output, ",\n\"columns\" : [ ");
if (name_list) {
cg_json_name_list(output, name_list);
}
bprintf(output, " ]");
if (emit_values) {
// We only try to emit values if we know there is one row of them
// So the select statement can only be a values clause with only one list of values.
// This is guaranteed because of is_simple_insert(...) already checked this.
// The general insert form might have arguments in all sorts of places and
// so it can't be readily analyzed by downstream tools. This very simple
// insert form can be manipulated in interesting ways. A downstream tool might
// want to convert it into an upsert or some such. In any case, we pull
// out the very simple inserts to allow them to be more deeply analyzed.
bprintf(output, ",\n\"values\" : [\n");
BEGIN_LIST;
BEGIN_INDENT(v1, 2);
if (is_ast_select_stmt(columns_values_right)) {
EXTRACT(select_stmt, columns_values_right);
EXTRACT_NOTNULL(select_core_list, select_stmt->left);
EXTRACT(select_core_compound, select_core_list->right);
EXTRACT_NOTNULL(select_core, select_core_list->left);
EXTRACT_NOTNULL(values, select_core->right);
columns_values_right = values->left;
}
Invariant(columns_values_right == NULL || is_ast_insert_list(columns_values_right));
EXTRACT(insert_list, columns_values_right);
for (ast_node *item = insert_list; item; item = item->right) {
COMMA;
bprintf(output, "{\n");
BEGIN_INDENT(v2, 2);
cg_fragment_with_params_raw(output, "value", item->left, gen_root_expr);
END_INDENT(v2);
bprintf(output, "\n}");
}
END_INDENT(v1);
END_LIST;
bprintf(output, "]");
}
}
// Delete statement gets the table name and the full statement and args
// as one fragment.
static void cg_json_delete_stmt(charbuf *output, ast_node * ast) {
Contract(is_delete_stmt(ast));
ast_node *delete_stmt = is_ast_with_delete_stmt(ast) ? ast->right : ast;
EXTRACT_ANY_NOTNULL(name_ast, delete_stmt->left);
// use the canonical name (which may be case-sensitively different)
CSTR name = name_ast->sem->sptr->struct_name;
bprintf(output, ",\n\"table\" : \"%s\"", name);
cg_fragment_with_params(output, "statement", ast, gen_one_stmt);
}
// Update statement gets the table name and the full statement and args
// as one fragment.
static void cg_json_update_stmt(charbuf *output, ast_node *ast) {
Contract(is_update_stmt(ast));
ast_node *update_stmt = is_ast_with_update_stmt(ast) ? ast->right : ast;
EXTRACT_ANY_NOTNULL(name_ast, update_stmt->left);
// use the canonical name (which may be case-sensitively different)
CSTR name = name_ast->sem->sptr->struct_name;
bprintf(output, ",\n\"table\" : \"%s\"", name);
cg_fragment_with_params(output, "statement", ast, gen_one_stmt);
}
// Start a new section for a proc, if testing we spew the test info here
// This lets us attribute the output to a particular line number in the test file.
static void cg_begin_proc(charbuf *output, ast_node *ast, ast_node *misc_attrs) {
Contract(is_ast_create_proc_stmt(ast));
EXTRACT_STRING(name, ast->left);
if (output->used > 1) bprintf(output, ",\n");
cg_json_test_details(output, ast, misc_attrs);
bprintf(output, "{\n");
}
// For symetry we have this lame end function
static void cg_end_proc(charbuf *output, ast_node *ast) {
bprintf(output, "\n}");
}
// Emit the arguments to the proc, track if they are valid (i.e. no OUT args)
// If not valid, the proc will be "general"
static bool_t cg_parameters(charbuf *output, ast_node *ast) {
Contract(is_ast_create_proc_stmt(ast));
EXTRACT_STRING(name, ast->left);
EXTRACT_NOTNULL(proc_params_stmts, ast->right);
EXTRACT(params, proc_params_stmts->left);
bool_t simple = 1;
bytebuf *arg_info = find_proc_arg_info(name);
CSTR *infos = arg_info ? (CSTR *)arg_info->ptr : NULL;
bprintf(output, "\"args\" : [\n");
BEGIN_INDENT(parms, 2);
simple = cg_json_params(output, params, infos);
END_INDENT(parms);
bprintf(output, "]");
return simple;
}
// The purpose of the "simple" versions is to enable code-rewriters to replace
// the proc with the DML directly and bind it. The code gen can some idea of what's
// going on in the simple cases -- it's a single row insert. In those cases it's
// possible to skip the C codegen entirely. You can just bind and run the DML.
bool_t static is_simple_insert(ast_node *ast) {
if (!is_ast_insert_stmt(ast)) {
return false;
}
EXTRACT_NOTNULL(name_columns_values, ast->right);
EXTRACT_NOTNULL(columns_values, name_columns_values->right);
if (!is_select_stmt(columns_values->right)) {
// the insert statement does not have a select statement
return true;
}
EXTRACT(select_stmt, columns_values->right);
EXTRACT_NOTNULL(select_core_list, select_stmt->left);
EXTRACT(select_core_compound, select_core_list->right);
if (select_core_compound != NULL) {
// The select statement is a compound select therefore it's not simple insert
return false;
}
EXTRACT_NOTNULL(select_core, select_core_list->left);
if (!is_ast_values(select_core->right)) {
// The select statement does not have VALUES clause then it's not simple insert
return false;
}
EXTRACT_NOTNULL(values, select_core->right);
if (values->right) {
// The values clause has multiple list of value therefore it's not a simple insert
return false;
}
// The insert statement contains a select statement that only has a VALUES clause
// and the VALUES clause has only one list of values.
return true;
}
static void cg_json_general_proc(ast_node *ast, ast_node *misc_attrs, CSTR params) {
charbuf *output = general;
cg_begin_proc(output, ast, misc_attrs);
sem_t sem_type = ast->sem->sem_type;
BEGIN_INDENT(proc, 2)
bprintf(output, "%s", params);
bool_t has_any_result_set = !!ast->sem->sptr;
bool_t uses_out_union = !!(sem_type & SEM_TYPE_USES_OUT_UNION);
bool_t uses_out = !!(sem_type & SEM_TYPE_USES_OUT);
bool_t select_result = !uses_out && !uses_out_union && has_any_result_set;
if (has_any_result_set) {
cg_json_projection(output, ast);
}
// clearer coding of the result types including out union called out seperately
if (uses_out) {
Invariant(has_any_result_set);
bprintf(output, ",\n\"hasOutResult\" : 1");
}
else if (uses_out_union) {
Invariant(has_any_result_set);
bprintf(output, ",\n\"hasOutUnionResult\" : 1");
}
else if (select_result) {
Invariant(has_any_result_set);
bprintf(output, ",\n\"hasSelectResult\" : 1");
}
else {
Invariant(!has_any_result_set);
}
bprintf(output, ",\n\"usesDatabase\" : %d", !!(sem_type & SEM_TYPE_DML_PROC));
END_INDENT(proc);
}
// For procedures and triggers we want to walk the statement list and emit a set
// of dependency entries that show what the code in question is using and how.
// We track tables that are used and if they appear in say the FROM clause
// (or some other read-context) or if they are the subject of an insert, update,
// or delete. We also track the use of nested procedures and produce a list of
// procs the subject might call. Of course no proc calls ever appear in triggers.
static void cg_json_dependencies(charbuf *output, ast_node *ast) {
json_context context;
CHARBUF_OPEN(used_tables);
CHARBUF_OPEN(used_views);
CHARBUF_OPEN(insert_tables);
CHARBUF_OPEN(update_tables);
CHARBUF_OPEN(delete_tables);
CHARBUF_OPEN(from_tables);
CHARBUF_OPEN(used_procs);
context.cookie = cookie_str;
context.proc_ast = ast;
context.used_tables = &used_tables;
context.used_views = &used_views;
context.insert_tables = &insert_tables;
context.delete_tables = &delete_tables;
context.update_tables = &update_tables;
context.from_tables = &from_tables;
context.used_procs = &used_procs;
table_callbacks callbacks = {
.notify_table_or_view_drops = false,
.notify_fk = false,
.notify_triggers = false,
.callback_any_table = cg_found_table,
.callback_any_view = cg_found_view,
.callback_inserts = cg_found_insert,
.callback_updates = cg_found_update,
.callback_deletes = cg_found_delete,
.callback_from = cg_found_from,
.callback_proc = cg_found_proc,
.callback_context = &context,
};
find_table_refs(&callbacks, ast);
if (insert_tables.used > 1) {
bprintf(output, ",\n\"insertTables\" : [ %s ]", insert_tables.ptr);
}
if (update_tables.used > 1) {
bprintf(output, ",\n\"updateTables\" : [ %s ]", update_tables.ptr);
}
if (delete_tables.used > 1) {
bprintf(output, ",\n\"deleteTables\" : [ %s ]", delete_tables.ptr);
}
if (from_tables.used > 1) {
bprintf(output, ",\n\"fromTables\" : [ %s ]", from_tables.ptr);
}
if (used_procs.used > 1) {
bprintf(output, ",\n\"usesProcedures\" : [ %s ]", used_procs.ptr);
}
if (used_views.used > 1) {
bprintf(output, ",\n\"usesViews\" : [ %s ]", used_views.ptr);
}
bprintf(output, ",\n\"usesTables\" : [ %s ]", used_tables.ptr);
CHARBUF_CLOSE(used_procs);
CHARBUF_CLOSE(from_tables);
CHARBUF_CLOSE(delete_tables);
CHARBUF_CLOSE(update_tables);
CHARBUF_CLOSE(insert_tables);
CHARBUF_CLOSE(used_views);
CHARBUF_CLOSE(used_tables);
}
// If we find a procedure definition we crack its arguments and first statement
// If it matches one of the known types we generate the details for it. Otherwise
// it goes into the general bucket. The output is redirected to the appropriate
// output stream for the type of statement and then a suitable helper is dispatched.
// Additionally, each procedure includes an array of tables that it uses regardless
// of the type of procedure.
static void cg_json_create_proc(ast_node *ast, ast_node *misc_attrs) {
Contract(is_ast_create_proc_stmt(ast));
EXTRACT_ANY_NOTNULL(name_ast, ast->left);
EXTRACT_STRING(name, name_ast);
EXTRACT_NOTNULL(proc_params_stmts, ast->right);
EXTRACT(params, proc_params_stmts->left);
EXTRACT(stmt_list, proc_params_stmts->right);
// shared fragments are invisible to the JSON or anything else, they have
// no external interface.
uint32_t frag_type = find_proc_frag_type(ast);
if (frag_type == FRAG_TYPE_SHARED) {
return;
}
CHARBUF_OPEN(param_buffer);
charbuf *output = ¶m_buffer;
bprintf(output, "\"name\" : \"%s\",\n", name);
CHARBUF_OPEN(tmp);
// quote the file as a json style literaj
CSTR filename = name_ast->filename;
#ifdef _WIN32
CSTR slash = strrchr(filename, '\\');
#else
CSTR slash = strrchr(filename, '/');
#endif
if (slash) {
filename = slash + 1;
}
cg_encode_json_string_literal(filename, &tmp);
bprintf(output, "\"definedInFile\" : %s,\n", tmp.ptr);
CHARBUF_CLOSE(tmp);
bool_t simple = cg_parameters(output, ast);
cg_json_dependencies(output, ast);
if (ast->sem->region) {
cg_json_emit_region_info(output, ast);
}
if (misc_attrs) {
bprintf(output, ",\n");
cg_json_misc_attrs(output, misc_attrs);
}
if (!stmt_list) {
output = general;
cg_json_general_proc(ast, misc_attrs, param_buffer.ptr);
goto cleanup;
}
EXTRACT_STMT_AND_MISC_ATTRS(stmt, nested_misc_attrs, stmt_list);
// if more than one statement it isn't simple
if (stmt_list->right) {
simple = 0;
}
// we have to see if it uses shared fragments, this can't be "simple"
// because the parameters can be synthetic and require assignments and such
if (simple && is_select_stmt(stmt)) {
found_shared_fragment = false;
CHARBUF_OPEN(scratch);
cg_json_select_stmt(&scratch, stmt); // easy way to walk the tree
CHARBUF_CLOSE(scratch);
simple = !found_shared_fragment;
}
if (simple && is_select_stmt(stmt)) {
output = queries;
cg_begin_proc(output, ast, misc_attrs);
BEGIN_INDENT(proc, 2);
bprintf(output, "%s", param_buffer.ptr);
cg_json_projection(output, stmt);
cg_json_select_stmt(output, stmt);
END_INDENT(proc);
}
else if (simple && is_insert_stmt(stmt)) {
bool_t simple_insert = is_simple_insert(stmt);
output = simple_insert ? inserts : general_inserts;
cg_begin_proc(output, ast, misc_attrs);
BEGIN_INDENT(proc, 2);
bprintf(output, "%s", param_buffer.ptr);
cg_json_insert_stmt(output, stmt, simple_insert);
END_INDENT(proc);
}
else if (simple && is_delete_stmt(stmt)) {
output = deletes;
cg_begin_proc(output, ast, misc_attrs);
BEGIN_INDENT(proc, 2);
bprintf(output, "%s", param_buffer.ptr);
cg_json_delete_stmt(output, stmt);
END_INDENT(proc);
}
else if (simple && is_update_stmt(stmt)) {
output = updates;
cg_begin_proc(output, ast, misc_attrs);
BEGIN_INDENT(proc, 2);
bprintf(output, "%s", param_buffer.ptr);
cg_json_update_stmt(output, stmt);
END_INDENT(proc);
}
else {
output = general;
cg_json_general_proc(ast, misc_attrs, param_buffer.ptr);
}
cleanup:
CHARBUF_CLOSE(param_buffer);
cg_end_proc(output, ast);
}
// This lets us have top level attributes that go into the main output stream
// this is stuff like the name of the database and so forth. By convention these
// are placed as an attribution on the statements "declare database object".
// Attributes for any object variables named *database are unified so that
// different schema fragments can contribute easily.
static void cg_json_declare_vars_type(charbuf *output, ast_node *ast, ast_node *misc_attrs) {
Contract(is_ast_declare_vars_type(ast));
EXTRACT_NOTNULL(name_list, ast->left);
EXTRACT_ANY_NOTNULL(data_type, ast->right);
bool_t first_attr = output->used == 1;
// we're looking for "declare database object" and nothing else
if (misc_attrs && !name_list->right && is_object(data_type->sem->sem_type)) {
EXTRACT_STRING(name, name_list->left);
if (Strendswith(name, "database")) {
if (first_attr) {
bprintf(output, "\n");
}
cg_json_test_details(output, ast, misc_attrs);
BEGIN_INDENT(attr, 2);
// The attributes from all the various sources are unified, they will
// go into one attributes block. Note there can be duplicates but that's
// not a problem for the schema and may even be desired. Note also
// that even if we had only one such object there could still be duplicates
// because again, attributes on a single object are not unique. They mean
// whatever you want them to mean. So we just spit them out and let
// the consumer sort it out. In practice this isn't really a problem.
// Whatever tool is downstream will complain if the attributes are badly formed.
for (ast_node *item = misc_attrs; item; item = item->right) {
if (!first_attr) {
bprintf(output, ",\n");
}
first_attr = false;
cg_json_misc_attr(output, item->left);
}
END_INDENT(attr);
}
}
}
// Here we create several buffers for the various statement types and then redirect
// output into the appropriate buffer as we walk the statements. Finally each buffer
// is emitted in order.
static void cg_json_stmt_list(charbuf *output, ast_node *head) {
CHARBUF_OPEN(query_buf);
CHARBUF_OPEN(insert_buf);
CHARBUF_OPEN(update_buf);
CHARBUF_OPEN(delete_buf);
CHARBUF_OPEN(general_buf);
CHARBUF_OPEN(general_inserts_buf);
CHARBUF_OPEN(attributes_buf);
queries = &query_buf;
inserts = &insert_buf;
updates = &update_buf;
deletes = &delete_buf;
general = &general_buf;
general_inserts = &general_inserts_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)) {
cg_json_create_proc(stmt, misc_attrs);
}
else if (is_ast_declare_vars_type(stmt)) {
cg_json_declare_vars_type(&attributes_buf, stmt, misc_attrs);
}
}
bprintf(output, "\"attributes\" : [");
bprintf(output, "%s", attributes_buf.ptr);
bprintf(output, "\n],\n");
bprintf(output, "\"queries\" : [\n");
bindent(output, queries, 2);
bprintf(output, "\n],\n");
bprintf(output, "\"inserts\" : [\n");
bindent(output, inserts, 2);
bprintf(output, "\n],\n");
bprintf(output, "\"generalInserts\" : [\n");
bindent(output, general_inserts, 2);
bprintf(output, "\n],\n");
bprintf(output, "\"updates\" : [\n");
bindent(output, updates, 2);
bprintf(output, "\n],\n");
bprintf(output, "\"deletes\" : [\n");
bindent(output, deletes, 2);
bprintf(output, "\n],\n");
bprintf(output, "\"general\" : [\n");
bindent(output, general, 2);
bprintf(output, "\n]");
CHARBUF_CLOSE(attributes_buf);
CHARBUF_CLOSE(general_inserts_buf);
CHARBUF_CLOSE(general_buf);
CHARBUF_CLOSE(delete_buf);
CHARBUF_CLOSE(update_buf);
CHARBUF_CLOSE(insert_buf);
CHARBUF_CLOSE(query_buf);
// Ensure the globals do not hold any pointers so that leaksan will find any leaks
// All of these have already been freed (above)
queries = NULL;
deletes = NULL;
inserts = NULL;
updates = NULL;
general = NULL;
general_inserts = NULL;
}
// Here we emit a top level fragment that has all the tables and
// all the procedures that use that table. This is the reverse mapping
// from the proc section where each proc defines which tables it uses.
// i.e. we can use this map to go from a dirty table name to a list of
// affected queries/updates etc.
static void cg_json_table_users(charbuf *output) {
uint32_t count = tables_to_procs->count;
symtab_entry *deps = symtab_copy_sorted_payload(tables_to_procs, default_symtab_comparator);
bprintf(output, "\"tableUsers\" : {\n");
BEGIN_INDENT(users, 2);
BEGIN_LIST;
for (int32_t i = 0; i < count; i++) {
CSTR sym = deps[i].sym;
charbuf *buf = (charbuf*)deps[i].val;
COMMA;
bprintf(output, "\"%s\" : [ %s ]", sym, buf->ptr);
}
END_LIST;
END_INDENT(users);
bprintf(output, "}");
free(deps);
}
// Main entry point for json schema format
cql_noexport void cg_json_schema_main(ast_node *head) {
Contract(options.file_names_count == 1);
cql_exit_on_semantic_errors(head);
tables_to_procs = symtab_new();
CHARBUF_OPEN(main);
charbuf *output = &main;
bprintf(output, "%s", rt->source_prefix);
// master dictionary begins
bprintf(output, "\n{\n");
BEGIN_INDENT(defs, 2);
cg_json_tables(output);
bprintf(output, ",\n");
cg_json_virtual_tables(output);
bprintf(output, ",\n");
cg_json_views(output);
bprintf(output, ",\n");
cg_json_indices(output);
bprintf(output, ",\n");
cg_json_triggers(output);
bprintf(output, ",\n");
cg_json_stmt_list(output, head);
bprintf(output, ",\n");
cg_json_regions(output);
bprintf(output, ",\n");
cg_json_ad_hoc_migration_procs(output);
bprintf(output, ",\n");
cg_json_enums(output);
bprintf(output, ",\n");
cg_json_constant_groups( output);
bprintf(output, ",\n");
cg_json_subscriptions( output);
if (options.test) {
bprintf(output, ",\n");
cg_json_table_users(output);
}
END_INDENT(defs);
bprintf(output, "\n}\n");
cql_write_file(options.file_names[0], output->ptr);
CHARBUF_CLOSE(main);
SYMTAB_CLEANUP(tables_to_procs);
}
#endif