UNPKG

@selfage/generator_cli

Version:

Code generation for message, service, and database.

1,042 lines (1,040 loc) 187 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpannerDatabaseGenerator = void 0; const output_content_builder_1 = require("./output_content_builder"); const util_1 = require("./util"); let COLUMN_PRIMITIVE_TYPE_TO_TS_TYPE = new Map([ ["bool", "boolean"], ["int53", "number"], ["float64", "number"], ["timestamp", "number"], ["string", "string"], ]); let COLUMN_PRIMITIVE_TYPE_TO_TABLE_TYPE = new Map([ ["bool", "BOOL"], ["int53", "INT64"], ["float64", "FLOAT64"], ["timestamp", "TIMESTAMP"], ["string", "STRING(MAX)"], ]); let COLUMN_PRIMITIVE_TYPE_TO_QUERY_TYPE = new Map([ ["bool", "bool"], ["int53", "int64"], ["float64", "float64"], ["timestamp", "timestamp"], ["string", "string"], ]); let BINARY_OP_NAME = new Map([ [">", "Gt"], [">=", "Ge"], ["<", "Lt"], ["<=", "Le"], ["=", "Eq"], ["!=", "Ne"], ["SEARCH", "Search"], ["SCORE", "Score"], ["IN", "In"], ]); let ALL_JOIN_LEAF_OP = new Set() .add(">") .add("<") .add(">=") .add("<=") .add("!=") .add("="); let ALL_JOIN_TYPE = new Set() .add("INNER") .add("CROSS") .add("FULL") .add("LEFT") .add("RIGHT"); let SCORE_RESULT_OP = new Set().add(">").add(">=").add("<").add("<=").add("="); function getColumnDefinition(loggingPrefix, columnName, table) { for (let column of table.columns) { if (column.name === columnName) { return column; } } throw new Error(`${loggingPrefix} column ${columnName} is not found in the table ${table.name}.`); } function getColumnGroupDefinition(loggingPrefix, columnGroupName, table) { for (let columnGroup of table.columnGroups) { if (columnGroup.name === columnGroupName) { return columnGroup; } } throw new Error(`${loggingPrefix} column group ${columnGroupName} is not found in the table ${table.name}.`); } function getSearchColumnDefinition(loggingPrefix, searchColumnName, table) { if (table.searchColumns) { for (let column of table.searchColumns) { if (column.name === searchColumnName) { return column; } } } throw new Error(`${loggingPrefix} search column ${searchColumnName} is not found in the table ${table.name}.`); } class SpannerDatabaseGenerator { constructor(definitionModulePath, spannerDatabaseDefinition, definitionResolver, outputContentMap) { this.definitionModulePath = definitionModulePath; this.spannerDatabaseDefinition = spannerDatabaseDefinition; this.definitionResolver = definitionResolver; this.outputContentMap = outputContentMap; this.databaseTables = new Map(); this.tableDdls = new Array(); this.outputFields = new Array(); this.outputConversions = new Array(); this.outputFieldDescriptors = new Array(); } generate() { if (!this.spannerDatabaseDefinition.name) { throw new Error(`"name" is missing on a spannerDatabase.`); } if (!this.spannerDatabaseDefinition.outputDdl) { throw new Error(`"outputDdl" is missing on spannerDatabase ${this.spannerDatabaseDefinition.name}.`); } this.ddlContentBuilder = output_content_builder_1.SimpleContentBuilder.get(this.outputContentMap, ".json", this.spannerDatabaseDefinition.outputDdl); if (!this.spannerDatabaseDefinition.outputSql) { throw new Error(`"outputSql" is missing on spannerDatabase ${this.spannerDatabaseDefinition.name}.`); } this.sqlContentBuilder = output_content_builder_1.TsContentBuilder.get(this.outputContentMap, this.definitionModulePath, this.spannerDatabaseDefinition.outputSql); if (!this.spannerDatabaseDefinition.tables) { throw new Error(`"table" is missing on spannerDatabase ${this.spannerDatabaseDefinition.name}.`); } for (let table of this.spannerDatabaseDefinition.tables) { if (table.kind === "Table") { this.generateSpannerTable(table); } else if (table.kind === "TaskTable") { this.generateSpannerTaskTable(table); } } this.ddlContentBuilder.push(`{ "tables": [${this.tableDdls.join(", ")}] }`); if (this.spannerDatabaseDefinition.inserts) { for (let insertDefinition of this.spannerDatabaseDefinition.inserts) { this.generateSpannerInsert(insertDefinition); } } if (this.spannerDatabaseDefinition.updates) { for (let updateDefinition of this.spannerDatabaseDefinition.updates) { this.generateSpannerUpdate(updateDefinition); } } if (this.spannerDatabaseDefinition.deletes) { for (let deleteDefinition of this.spannerDatabaseDefinition.deletes) { this.generateSpannerDelete(deleteDefinition); } } if (this.spannerDatabaseDefinition.selects) { for (let selectDefinition of this.spannerDatabaseDefinition.selects) { this.generateSpannerSelect(selectDefinition); } } } generateSpannerTable(table) { if (!table.name) { throw new Error(`"name" is missing on a spanner table.`); } let loggingPrefix = `When generating DDL for table ${table.name},`; let addColumnDdls = new Array(); let createColumnPartialDdls = new Array(); if (!table.columns) { throw new Error(`${loggingPrefix} "columns" is missing.`); } for (let column of table.columns) { if (!column.name) { throw new Error(`${loggingPrefix} "name" is mssing on a column.`); } if (!column.type) { throw new Error(`${loggingPrefix} "type" is missing on column ${column.name}.`); } let type = COLUMN_PRIMITIVE_TYPE_TO_TABLE_TYPE.get(column.type); if (!type) { let definition = this.definitionResolver.resolve(loggingPrefix, column.type, column.import); if (definition.kind === "Enum") { type = "FLOAT64"; } else if (definition.kind === "Message") { type = "BYTES(MAX)"; } } let partialDdl = `${column.name} ${column.isArray ? "Array<" + type + ">" : type}${column.nullable ? "" : " NOT NULL"}`; createColumnPartialDdls.push(partialDdl); addColumnDdls.push(`{ "name": "${column.name}", "addColumnDdl": "ALTER TABLE ${table.name} ADD COLUMN ${partialDdl}" }`); } if (table.searchColumns) { for (let searchColumn of table.searchColumns) { if (!searchColumn.name) { throw new Error(`${loggingPrefix} "name" is missing in one element of "searchColumns" field.`); } if (!searchColumn.columnRefs) { throw new Error(`${loggingPrefix} "columnRefs" is missing in search column ${searchColumn.name}.`); } let columnsConcats = new Array(); for (let columnRef of searchColumn.columnRefs) { let columnDef = getColumnDefinition(loggingPrefix + " and when generating search column ref,", columnRef, table); // Only string type for now. if (columnDef.type !== "string") { throw new Error(`${loggingPrefix} column ${columnRef} is not a string and cannot be used in a search column.`); } if (columnDef.isArray) { throw new Error(`${loggingPrefix} column ${columnRef} is an array and cannot be used in a search column.`); } columnsConcats.push(columnRef); } let partialDdl = `${searchColumn.name} TOKENLIST AS (TOKENIZE_FULLTEXT(${columnsConcats.join(" || ' ' || ")})) HIDDEN`; createColumnPartialDdls.push(partialDdl); addColumnDdls.push(`{ "name": "${searchColumn.name}", "addColumnDdl": "ALTER TABLE ${table.name} ADD COLUMN ${partialDdl}" }`); } } let primaryKeys = new Array(); if (!table.primaryKeys) { throw new Error(`${loggingPrefix} "primaryKeys" is missing.`); } for (let i = 0; i < table.primaryKeys.length; i++) { let key = table.primaryKeys[i]; if (typeof key === "string") { key = { name: key, desc: false, }; } if (key.desc == null) { throw new Error(`${loggingPrefix} "desc" is missing in primary key ${key.name}.`); } table.primaryKeys[i] = key; if (!key.name) { throw new Error(`${loggingPrefix} "name" is missing in "primaryKeys" field.`); } let columnDefinition = getColumnDefinition(loggingPrefix + " and when generating primary keys,", key.name, table); if (columnDefinition.isArray) { throw new Error(`${loggingPrefix} column ${key} is an array and cannot be used as a primary key.`); } primaryKeys.push(`${key.name} ${key.desc ? "DESC" : "ASC"}`); } let interleaveOption = ""; if (table.interleave) { if (!table.interleave.parentTable) { throw new Error(`${loggingPrefix} "parentTable" is missing in "interleave" field.`); } let parentTable = this.databaseTables.get(table.interleave.parentTable); if (!parentTable) { throw new Error(`${loggingPrefix} the parent table ${table.interleave.parentTable} is not found in the database.`); } if (parentTable.primaryKeys.length >= table.primaryKeys.length) { throw new Error(`${loggingPrefix} pimary keys of the child table should be more than the parent table ${table.interleave.parentTable}.`); } for (let i = 0; i < parentTable.primaryKeys.length; i++) { let parentKey = parentTable.primaryKeys[i]; let childKey = table.primaryKeys[i]; if (parentKey.name !== childKey.name) { throw new Error(`${loggingPrefix} at position ${i}, pimary key "${childKey.name}" doesn't match the key "${parentKey.name}" of the parent table.`); } if (parentKey.desc !== childKey.desc) { throw new Error(`${loggingPrefix} at position ${i}, pimary key "${childKey.name}" is ${childKey.desc ? "DESC" : "ASC"} which doesn't match the key of the parent table.`); } let parentColumnDefinition = getColumnDefinition(loggingPrefix + "and when validating interleaving,", parentKey.name, parentTable); let childColumnDefinition = getColumnDefinition(loggingPrefix + "and when validating interleaving,", childKey.name, table); if (parentColumnDefinition.type !== childColumnDefinition.type) { throw new Error(`${loggingPrefix} at position ${i}, primary key ${childColumnDefinition.name}'s type "${childColumnDefinition.type}" doesn't match the type "${parentColumnDefinition.type}" of the parent table. `); } } interleaveOption = `, INTERLEAVE IN PARENT ${table.interleave.parentTable}${table.interleave.cascadeOnDelete ? " ON DELETE CASCADE" : ""}`; } let indexDdls = new Array(); if (table.indexes) { for (let index of table.indexes) { if (!index.name) { throw new Error(`${loggingPrefix} "name" is missing in one element of "indexes" field.`); } if (!index.columns) { throw new Error(`${loggingPrefix} "columns" is missing in index ${index.name}.`); } let indexColumns = new Array(); for (let column of index.columns) { if (typeof column === "string") { column = { name: column, desc: false, }; } if (column.desc == null) { throw new Error(`${loggingPrefix} "desc" is missing in index column ${column.name}.`); } getColumnDefinition(loggingPrefix + " and when generating indexes,", column.name, table); indexColumns.push(`${column.name}${column.desc ? " DESC" : ""}`); } indexDdls.push(`{ "name": "${index.name}", "createIndexDdl": "CREATE ${index.unique ? "UNIQUE " : ""}${index.nullFiltered ? "NULL_FILTERED " : ""}INDEX ${index.name} ON ${table.name}(${indexColumns.join(", ")})" }`); } } if (table.searchIndexes) { for (let searchIndex of table.searchIndexes) { if (!searchIndex.name) { throw new Error(`${loggingPrefix} "name" is missing in one element of "searchIndexes" field.`); } if (!searchIndex.columns) { throw new Error(`${loggingPrefix} "columns" is missing in search index ${searchIndex.name}.`); } for (let column of searchIndex.columns) { getSearchColumnDefinition(loggingPrefix + " and when generating search indexes,", column, table); } let paritiionByClause = ""; if (searchIndex.partitionByColumns) { for (let column of searchIndex.partitionByColumns) { getColumnDefinition(loggingPrefix + " and when generating search indexes with partition by clauses,", column, table); } paritiionByClause = ` PARTITION BY ${searchIndex.partitionByColumns.join(", ")}`; } let orderByClause = ""; if (searchIndex.orderByColumns) { let orderByColumns = new Array(); for (let column of searchIndex.orderByColumns) { if (typeof column === "string") { column = { name: column, desc: false, }; } let columnDef = getColumnDefinition(loggingPrefix + " and when generating search indexes with order by clauses,", column.name, table); if (columnDef.type !== "int53") { throw new Error(`${loggingPrefix} search index ${searchIndex.name}'s order by column ${column.name} is not an int53. Search index can only be ordered by ints.`); } if (columnDef.isArray) { throw new Error(`${loggingPrefix} search index ${searchIndex.name}'s order by column ${column.name} is an array. Search index can only be ordered by non-array columns.`); } if (columnDef.nullable) { throw new Error(`${loggingPrefix} search index ${searchIndex.name}'s order by column ${column.name} is nullable. Search index can only be ordered by non-null columns.`); } orderByColumns.push(`${column.name}${column.desc ? " DESC" : ""}`); } orderByClause = ` ORDER BY ${orderByColumns.join(", ")}`; } indexDdls.push(`{ "name": "${searchIndex.name}", "createIndexDdl": "CREATE SEARCH INDEX ${searchIndex.name} ON ${table.name}(${searchIndex.columns.join(", ")})${paritiionByClause}${orderByClause}" }`); } } this.databaseTables.set(table.name, table); this.tableDdls.push(`{ "name": "${table.name}", "columns": [${addColumnDdls.join(", ")}], "createTableDdl": "CREATE TABLE ${table.name} (${createColumnPartialDdls.join(", ")}) PRIMARY KEY (${primaryKeys.join(", ")})${interleaveOption}", "indexes": [${indexDdls.join(", ")}] }`); if (table.insert) { this.generateSpannerInsert({ name: `${table.insert}`, table: table.name, set: table.columns.map((column) => column.name), }); } if (table.delete) { this.generateSpannerDelete({ name: table.delete, table: table.name, where: { op: "AND", exprs: table.primaryKeys.map((key) => ({ lColumn: typeof key === "string" ? key : key.name, op: "=", })), }, }); } if (table.get) { this.generateSpannerSelect({ name: table.get, from: table.name, where: { op: "AND", exprs: table.primaryKeys.map((key) => ({ lColumn: typeof key === "string" ? key : key.name, op: "=", })), }, get: table.columns.map((column) => column.name), }); } if (table.update) { let primaryKeys = table.primaryKeys.map((key) => typeof key === "string" ? key : key.name); this.generateSpannerUpdate({ name: table.update, table: table.name, where: { op: "AND", exprs: primaryKeys.map((key) => ({ lColumn: key, op: "=", })), }, set: table.columns .map((column) => column.name) .filter((name) => !primaryKeys.includes(name)), }); } } generateSpannerTaskTable(table) { if (!table.name) { throw new Error(`"name" is missing on a Spanner task table.`); } let loggingPrefix = `When coverting task table ${table.name} to Spanner table definition,`; if (!table.columns) { throw new Error(`${loggingPrefix} "columns" is missing on task table ${table.name}.`); } if (!table.retryCountColumn) { throw new Error(`${loggingPrefix} "retryCountColumn" is missing on task table ${table.name}.`); } if (!table.executionTimeColumn) { throw new Error(`${loggingPrefix} "executionTimeColumn" is missing on task table ${table.name}.`); } if (!table.createdTimeColumn) { throw new Error(`${loggingPrefix} "createdTimeColumn" is missing on task table ${table.name}.`); } let columns = [...table.columns]; columns.push({ name: table.retryCountColumn, type: "float64", nullable: true, }, { name: table.executionTimeColumn, type: "timestamp", nullable: true, }, { name: table.createdTimeColumn, type: "timestamp", nullable: true, }); if (!table.executionTimeIndex) { throw new Error(`${loggingPrefix} "executionTimeIndex" is missing on task table ${table.name}.`); } let indexes = [ ...(table.indexes ?? []), { name: table.executionTimeIndex, columns: [table.executionTimeColumn], }, ]; if (!table.insert) { throw new Error(`${loggingPrefix} "insert" is missing on task table ${table.name}.`); } if (!table.delete) { throw new Error(`${loggingPrefix} "delete" is missing on task table ${table.name}.`); } if (!table.get) { throw new Error(`${loggingPrefix} "get" is missing on task table ${table.name}.`); } this.generateSpannerTable({ kind: "Table", name: table.name, columns: columns, primaryKeys: table.primaryKeys, indexes: indexes, insert: table.insert, delete: table.delete, get: table.get, }); if (!table.listPendingTasks) { throw new Error(`${loggingPrefix} "listPendingTasks" is missing on task table ${table.name}.`); } this.generateSpannerSelect({ name: table.listPendingTasks, from: table.name, where: { op: "<=", lColumn: table.executionTimeColumn, }, get: table.columns.map((column) => column.name), }); if (!table.getMetadata) { throw new Error(`${loggingPrefix} "getMetadata" is missing on task table ${table.name}.`); } this.generateSpannerSelect({ name: table.getMetadata, from: table.name, where: { op: "AND", exprs: table.primaryKeys.map((key) => ({ lColumn: typeof key === "string" ? key : key.name, op: "=", })), }, get: [table.retryCountColumn, table.executionTimeColumn], }); if (!table.updateMetadata) { throw new Error(`${loggingPrefix} "updateMetadata" is missing on task table ${table.name}.`); } this.generateSpannerUpdate({ name: table.updateMetadata, table: table.name, where: { op: "AND", exprs: table.primaryKeys.map((key) => ({ lColumn: typeof key === "string" ? key : key.name, op: "=", })), }, set: [table.retryCountColumn, table.executionTimeColumn], }); } resolveTableAlias(loggingPrefix, tableAlias) { let tableName = this.currentTableAliases.get(tableAlias); if (!tableName) { throw new Error(`${loggingPrefix} ${tableAlias} refers to a table not found in the query.`); } return this.databaseTables.get(tableName); } generateSpannerInsert(insertDefinition) { if (!insertDefinition.name) { throw new Error(`"name" is missing on a spanner insert definition.`); } let loggingPrefix = `When generating insert statement ${insertDefinition.name},`; if (!insertDefinition.table) { throw new Error(`${loggingPrefix} "table" is missing.`); } let table = this.databaseTables.get(insertDefinition.table); if (!table) { throw new Error(`${loggingPrefix} table ${insertDefinition.table} is not found in the database's definition.`); } this.clearInput(); let columns = new Array(); let values = new Array(); if (!insertDefinition.set) { throw new Error(`${loggingPrefix} "set" is missing.`); } for (let column of insertDefinition.set) { let columnDefinition = getColumnDefinition(loggingPrefix, column, table); columns.push(column); let argVariable = column; this.collectInput(loggingPrefix, argVariable, columnDefinition); values.push(`@${argVariable}`); } let onConflictClause = ""; if (insertDefinition.onConflict === "IGNORE") { onConflictClause = "OR IGNORE "; } else if (insertDefinition.onConflict === "UPDATE") { onConflictClause = "OR UPDATE "; } this.sqlContentBuilder.importFromSpannerTransaction("Statement"); this.sqlContentBuilder.push(` export function ${(0, util_1.toInitalLowercased)(insertDefinition.name)}Statement( args: {${(0, util_1.joinArray)(this.inputArgs, "\n ", ",")} } ): Statement { return { sql: "INSERT ${onConflictClause}${insertDefinition.table} (${columns.join(", ")}) VALUES (${values.join(", ")})", params: {${(0, util_1.joinArray)(this.inputConversions, "\n ", ",")} }, types: {${(0, util_1.joinArray)(this.inputQueryTypes, "\n ", ",")} } }; } `); } generateSpannerUpdate(updateDefinition) { if (!updateDefinition.name) { throw new Error(`"name" is missing on a spanner update definition.`); } let loggingPrefix = `When generating update statement ${updateDefinition.name},`; if (!updateDefinition.table) { throw new Error(`${loggingPrefix} "table" is missing.`); } let table = this.databaseTables.get(updateDefinition.table); if (!table) { throw new Error(`${loggingPrefix} table ${updateDefinition.table} is not found in the database's definition.`); } this.clearInput(); if (!updateDefinition.table) { throw new Error(`${loggingPrefix} "table" is missing.`); } this.currentDefaultTableAlias = updateDefinition.table; this.currentTableAliases = new Map().set(updateDefinition.table, updateDefinition.table); if (!updateDefinition.where) { throw new Error(`${loggingPrefix} "where" is missing.`); } let whereClause = this.generateWhere(loggingPrefix + " and when generating where clause,", updateDefinition.where); let setItems = new Array(); if (!updateDefinition.set) { throw new Error(`${loggingPrefix} "set" is missing.`); } for (let column of updateDefinition.set) { let columnDefinition = getColumnDefinition(loggingPrefix, column, table); let argVariable = `set${(0, util_1.toInitialUppercased)(column)}`; this.collectInput(loggingPrefix + " and when generating set columns,", argVariable, columnDefinition); setItems.push(`${column} = @${argVariable}`); } this.sqlContentBuilder.importFromSpannerTransaction("Statement"); this.sqlContentBuilder.push(` export function ${(0, util_1.toInitalLowercased)(updateDefinition.name)}Statement( args: {${(0, util_1.joinArray)(this.inputArgs, "\n ", ",")} } ): Statement { return { sql: "UPDATE ${updateDefinition.table} SET ${setItems.join(", ")} WHERE ${whereClause}", params: {${(0, util_1.joinArray)(this.inputConversions, "\n ", ",")} }, types: {${(0, util_1.joinArray)(this.inputQueryTypes, "\n ", ",")} } }; } `); } generateSpannerDelete(deleteDefinition) { if (!deleteDefinition.name) { throw new Error(`"name" is missing on a spanner delete definition.`); } let loggingPrefix = `When generating delete statement ${deleteDefinition.name},`; let table = this.databaseTables.get(deleteDefinition.table); if (!table) { throw new Error(`${loggingPrefix} table ${deleteDefinition.table} is not found in the database's definition.`); } this.clearInput(); if (!deleteDefinition.table) { throw new Error(`${loggingPrefix} "table" is missing.`); } this.currentDefaultTableAlias = deleteDefinition.table; this.currentTableAliases = new Map().set(deleteDefinition.table, deleteDefinition.table); if (!deleteDefinition.where) { throw new Error(`${loggingPrefix} "where" is missing.`); } let whereClause = this.generateWhere(loggingPrefix, deleteDefinition.where); this.sqlContentBuilder.importFromSpannerTransaction("Statement"); this.sqlContentBuilder.push(` export function ${(0, util_1.toInitalLowercased)(deleteDefinition.name)}Statement( args: {${(0, util_1.joinArray)(this.inputArgs, "\n ", ",")} } ): Statement { return { sql: "DELETE ${deleteDefinition.table} WHERE ${whereClause}", params: {${(0, util_1.joinArray)(this.inputConversions, "\n ", ",")} }, types: {${(0, util_1.joinArray)(this.inputQueryTypes, "\n ", ",")} } }; } `); } generateSpannerSelect(selectDefinition) { if (!selectDefinition.name) { throw new Error(`"name" is missing on a spanner select definition.`); } let loggingPrefix = `When generating select statement ${selectDefinition.name},`; if (!selectDefinition.from) { throw new Error(`${loggingPrefix} "from" is missing.`); } if (!selectDefinition.as) { selectDefinition.as = selectDefinition.from; } if (!this.databaseTables.has(selectDefinition.from)) { throw new Error(`${loggingPrefix} table ${selectDefinition.from} is not found in the database.`); } this.clearInput(); this.currentDefaultTableAlias = selectDefinition.as; this.currentTableAliases = new Map().set(selectDefinition.as, selectDefinition.from); let fromTables = new Array(); fromTables.push(`${selectDefinition.from}${selectDefinition.as !== selectDefinition.from ? " AS " + selectDefinition.as : ""}`); if (selectDefinition.join) { for (let joinTable of selectDefinition.join) { if (!joinTable.with) { throw new Error(`${loggingPrefix} "with" is missing in "join" field.`); } if (!joinTable.as) { joinTable.as = joinTable.with; } if (!ALL_JOIN_TYPE.has(joinTable.type)) { throw new Error(`${loggingPrefix} and when joining ${joinTable.with}, "type" is either missing or not one of valid types "${Array.from(ALL_JOIN_TYPE).join(",")}"`); } this.currentTableAliases.set(joinTable.as, joinTable.with); this.currentJoinRightTableAlias = joinTable.as; this.currentJoinRightTable = this.databaseTables.get(joinTable.with); if (!this.currentJoinRightTable) { throw new Error(`${loggingPrefix} table ${joinTable.with} is not found in the database.`); } let joinOnClause = ""; if (joinTable.on) { joinOnClause = this.generateJoinOn(loggingPrefix + ` and when joining ${joinTable.with},`, joinTable.on); joinOnClause = ` ON ${joinOnClause}`; } fromTables.push(`${joinTable.type} JOIN ${joinTable.with}${joinTable.as !== joinTable.with ? " AS " + joinTable.as : ""}${joinOnClause}`); } } let whereClause = ""; if (selectDefinition.where) { whereClause = this.generateWhere(loggingPrefix + " and when generating where clause,", selectDefinition.where); whereClause = ` WHERE ${whereClause}`; } let orderByClause = ""; if (selectDefinition.orderBy) { let orderByExprs = new Array(); for (let i = 0; i < selectDefinition.orderBy.length; i++) { let expr = selectDefinition.orderBy[i]; if (typeof expr === "string") { expr = { column: expr, table: this.currentDefaultTableAlias, }; } if (!expr.table) { expr.table = this.currentDefaultTableAlias; } let table = this.resolveTableAlias(loggingPrefix + ` and when generating order by clause,`, expr.table); if (expr.func) { let { lExpr } = this.generateFunction(loggingPrefix + ` and when generating order by clause,`, expr.func, expr.column, expr.table, table, "OrderBy"); orderByExprs.push(`${lExpr}${expr.desc ? " DESC" : ""}`); } else { getColumnDefinition(loggingPrefix + ` and when generating order by clause,`, expr.column, table); orderByExprs.push(`${expr.table}.${expr.column}${expr.desc ? " DESC" : ""}`); } } orderByClause = ` ORDER BY ${orderByExprs.join(", ")}`; } let limitClause = ""; if (selectDefinition.withLimit) { this.collectInput(loggingPrefix, "limit", { type: "int53", }); limitClause = ` LIMIT @limit`; } let offsetClause = ""; if (selectDefinition.withOffset) { this.collectInput(loggingPrefix, "offset", { type: "int53", }); offsetClause = ` OFFSET @offset`; } this.clearOutput(); let selectColumns = new Array(); if (!selectDefinition.get) { throw new Error(`${loggingPrefix} "get" is missing.`); } for (let getExpr of selectDefinition.get) { if (typeof getExpr === "string") { getExpr = { column: getExpr, table: this.currentDefaultTableAlias, }; } if (!getExpr.table) { getExpr.table = this.currentDefaultTableAlias; } let table = this.resolveTableAlias(loggingPrefix + ` and when generating get columns,`, getExpr.table); if (getExpr.all) { let allColumns = table.columns; for (let column of allColumns) { let fieldName = `${(0, util_1.toInitalLowercased)(table.name)}${(0, util_1.toInitialUppercased)(column.name)}`; this.collectOuptut(loggingPrefix, fieldName, column); selectColumns.push(`${getExpr.table}.${column.name}`); } } else if (getExpr.columnGroup) { let columnGroupDefinition = getColumnGroupDefinition(loggingPrefix + ` and when generating get column groups,`, getExpr.columnGroup, table); for (let columnName of columnGroupDefinition.columns) { let columnDefinition = getColumnDefinition(loggingPrefix + ` and when generating get columns for column group ${getExpr.columnGroup},`, columnName, table); let fieldName = `${(0, util_1.toInitalLowercased)(table.name)}${(0, util_1.toInitialUppercased)(columnName)}`; this.collectOuptut(loggingPrefix, fieldName, columnDefinition); selectColumns.push(`${getExpr.table}.${columnName}`); } } else if (getExpr.func) { let { lExpr, returnType } = this.generateFunction(loggingPrefix + ` and when generating get columns,`, getExpr.func, getExpr.column, getExpr.table, table, "Select"); let fieldName = `${(0, util_1.toInitalLowercased)(table.name)}${(0, util_1.toInitialUppercased)(getExpr.column)}${BINARY_OP_NAME.get(getExpr.func)}`; this.collectOuptut(loggingPrefix, fieldName, { type: returnType, }); selectColumns.push(lExpr); } else { let columnDefinition = getColumnDefinition(loggingPrefix + ` and when generating select columns,`, getExpr.column, table); let fieldName = `${(0, util_1.toInitalLowercased)(table.name)}${(0, util_1.toInitialUppercased)(getExpr.column)}`; this.collectOuptut(loggingPrefix, fieldName, columnDefinition); selectColumns.push(`${getExpr.table}.${getExpr.column}`); } } this.sqlContentBuilder.importFromSpanner("Database", "Transaction"); this.sqlContentBuilder.importFromMessageDescriptor("MessageDescriptor"); this.sqlContentBuilder.push(` export interface ${selectDefinition.name}Row {${(0, util_1.joinArray)(this.outputFields, "\n ", ",")} } export let ${(0, util_1.toUppercaseSnaked)(selectDefinition.name)}_ROW: MessageDescriptor<${selectDefinition.name}Row> = { name: '${selectDefinition.name}Row', fields: [${this.outputFieldDescriptors.join(", ")}], }; export async function ${(0, util_1.toInitalLowercased)(selectDefinition.name)}( runner: Database | Transaction, args: {${(0, util_1.joinArray)(this.inputArgs, "\n ", ",")} } ): Promise<Array<${selectDefinition.name}Row>> { let [rows] = await runner.run({ sql: "SELECT ${selectColumns.join(", ")} FROM ${fromTables.join(" ")}${whereClause}${orderByClause}${limitClause}${offsetClause}", params: {${(0, util_1.joinArray)(this.inputConversions, "\n ", ",")} }, types: {${(0, util_1.joinArray)(this.inputQueryTypes, "\n ", ",")} } }); let resRows = new Array<${selectDefinition.name}Row>(); for (let row of rows) { resRows.push({${(0, util_1.joinArray)(this.outputConversions, "\n ", ",")} }); } return resRows; } `); } generateWhere(loggingPrefix, where) { if (where.op === "AND" || where.op === "OR") { return this.generateWhereConcat(loggingPrefix, where); } else { return this.generateWhereLeaf(loggingPrefix, where); } } generateWhereConcat(loggingPrefix, concat) { if (!concat.exprs || !Array.isArray(concat.exprs)) { throw new Error(`${loggingPrefix} "exprs" is either missing or not an array.`); } let clauses = concat.exprs.map((expr) => this.generateWhere(loggingPrefix, expr)); return "(" + clauses.join(` ${concat.op} `) + ")"; } generateWhereLeaf(loggingPrefix, leaf) { if (!leaf.lColumn) { throw new Error(`${loggingPrefix} "lColumn" is missing.`); } if (!leaf.lTable) { leaf.lTable = this.currentDefaultTableAlias; } let lTable = this.resolveTableAlias(loggingPrefix, leaf.lTable); if (leaf.func) { let { lExpr, returnType } = this.generateFunction(loggingPrefix, leaf.func, leaf.lColumn, leaf.lTable, lTable, `Where${BINARY_OP_NAME.get(leaf.op)}`); let argVariable = leaf.rVar ?? `${(0, util_1.toInitalLowercased)(lTable.name)}${(0, util_1.toInitialUppercased)(leaf.lColumn)}${BINARY_OP_NAME.get(leaf.func)}${BINARY_OP_NAME.get(leaf.op)}`; this.collectInput(loggingPrefix, argVariable, { type: returnType, }); switch (leaf.func) { case "SCORE": if (!SCORE_RESULT_OP.has(leaf.op)) { throw new Error(`${loggingPrefix} "op" is either missing or not one of valid types "${Array.from(SCORE_RESULT_OP).join(",")}" to handle SCORE results.`); } return `${lExpr} ${leaf.op} @${argVariable}`; default: throw new Error(`${loggingPrefix} function ${leaf.func} is not handled properly.`); } } else { if (leaf.op === "SEARCH") { getSearchColumnDefinition(loggingPrefix, leaf.lColumn, lTable); // Search column only supports string type for now. let argVariable = leaf.rVar ?? `${(0, util_1.toInitalLowercased)(lTable.name)}${(0, util_1.toInitialUppercased)(leaf.lColumn)}${BINARY_OP_NAME.get(leaf.op)}`; this.collectInput(loggingPrefix, argVariable, { type: "string", }); return `${leaf.op}(${leaf.lTable}.${leaf.lColumn}, @${argVariable})`; } else { let columnDefinition = getColumnDefinition(loggingPrefix, leaf.lColumn, lTable); switch (leaf.op) { case "IS NULL": case "IS NOT NULL": if (!columnDefinition.nullable) { throw new Error(`${loggingPrefix} column ${leaf.lTable}.${leaf.lColumn} is not nullable and doesn't need to check NULL in the query.`); } return `${leaf.lTable}.${leaf.lColumn} ${leaf.op}`; case ">": case "<": case ">=": case "<=": case "!=": case "=": if (columnDefinition.isArray) { throw new Error(`${loggingPrefix} column ${leaf.lTable}.${leaf.lColumn} is an array and doesn't support operator "${leaf.op}".`); } let argVariable = leaf.rVar ?? `${(0, util_1.toInitalLowercased)(lTable.name)}${(0, util_1.toInitialUppercased)(leaf.lColumn)}${BINARY_OP_NAME.get(leaf.op)}`; this.collectInput(loggingPrefix, argVariable, columnDefinition); return `${leaf.lTable}.${leaf.lColumn} ${leaf.op} @${argVariable}`; case "IN": if (columnDefinition.isArray) { throw new Error(`${loggingPrefix} column ${leaf.lTable}.${leaf.lColumn} is an array and doesn't support operator "IN".`); } let inArgVariable = leaf.rVar ?? `${(0, util_1.toInitalLowercased)(lTable.name)}${(0, util_1.toInitialUppercased)(leaf.lColumn)}${BINARY_OP_NAME.get(leaf.op)}`; this.collectInput(loggingPrefix, inArgVariable, { ...columnDefinition, isArray: true, }); return `${leaf.lTable}.${leaf.lColumn} ${leaf.op} @${inArgVariable}`; default: throw new Error(`${loggingPrefix} "op" is either missing or not valid.`); } } } } generateJoinOn(loggingPrefix, joinOn) { if (joinOn.op === "AND" || joinOn.op === "OR") { return this.generateJoinOnConcat(loggingPrefix, joinOn); } else { return this.generateJoinOnLeaf(loggingPrefix, joinOn); } } generateJoinOnConcat(loggingPrefix, concat) { if (!concat.exprs || !Array.isArray(concat.exprs)) { throw new Error(`${loggingPrefix} "exprs" is either missing or not an array.`); } let clauses = concat.exprs.map((expr) => this.generateJoinOn(loggingPrefix, expr)); return "(" + clauses.join(` ${concat.op} `) + ")"; } generateJoinOnLeaf(loggingPrefix, leaf) { if (!ALL_JOIN_LEAF_OP.has(leaf.op)) { throw new Error(`${loggingPrefix} "op" is either missing or not one of valid types "${Array.from(ALL_JOIN_LEAF_OP).join(",")}".`); } if (!leaf.rColumn) { throw new Error(`${loggingPrefix} "rColumn" is missing.`); } let rightColumnDefinition = getColumnDefinition(loggingPrefix, leaf.rColumn, this.currentJoinRightTable); if (leaf.lColumn) { if (!leaf.lTable) { throw new Error(`${loggingPrefix} "lTable" is missing.`); } let lTable = this.resolveTableAlias(loggingPrefix, leaf.lTable); let leftColumnDefinition = getColumnDefinition(loggingPrefix, leaf.lColumn, lTable); if (leftColumnDefinition.type !== rightColumnDefinition.type) { throw new Error(`${loggingPrefix} the left column ${leaf.lTable}.${leaf.lColumn} whose type is ${leftColumnDefinition.type} doesn't match the right column ${this.currentJoinRightTableAlias}.${leaf.rColumn} whose type is ${rightColumnDefinition.type}.`); } return `${leaf.lTable}.${leaf.lColumn} ${leaf.op} ${this.currentJoinRightTableAlias}.${leaf.rColumn}`; } else { let argVariable = leaf.lVar ?? `${(0, util_1.toInitalLowercased)(this.currentJoinRightTableAlias)}${(0, util_1.toInitialUppercased)(leaf.rColumn)}${BINARY_OP_NAME.get(leaf.op)}`; this.collectInput(loggingPrefix, argVariable, rightColumnDefinition); return `@${argVariable} ${leaf.op} ${this.currentJoinRightTableAlias}.${leaf.rColumn}`; } } generateFunction(loggingPrefix, func, lColumn, lTableAlias, lTable, context) { let argVariable = `${(0, util_1.toInitalLowercased)(lTable.name)}${(0, util_1.toInitialUppercased)(lColumn)}${BINARY_OP_NAME.get(func)}${context}`; switch (func) { case "SCORE": getSearchColumnDefinition(loggingPrefix, lColumn, lTable); // Search column only supports string type for now. this.collectInput(loggingPrefix, argVariable, { type: "string", }); // The function returns a number. return { lExpr: `${func}(${lTableAlias}.${lColumn}, @${argVariable})`, returnType: "float64", }; default: throw new Error(`${loggingPrefix} function ${func} is either missing or not valid.`); } } clearInput() { this.inputArgs = new Array(); this.inputQueryTypes = new Array(); this.inputConversions = new Array(); } collectInput(loggingPrefix, argVariable, columnType) { let tsType = COLUMN_PRIMITIVE_TYPE_TO_TS_TYPE.get(columnType.type); let queryType; let conversion; let argsDotVariable = `args.${argVariable}`; if (!tsType) { let typeDefinition = this.definitionResolver.resolve(loggingPrefix, columnType.type, columnType.import); this.sqlContentBuilder.importFromDefinition(columnType.import, columnType.type); if (typeDefinition.kind === "Enum") { this.sqlContentBuilder.importFromSpanner("Spanner"); if (!columnType.isArray) { tsType = typeDefinition.name; queryType = `{ type: "float64" }`; conversion = `Spanner.float(${argsDotVariable})`; } else { tsType = `Array<${typeDefinition.name}>`; queryType = `{ type: "array", child: { type: "float64" } }`; conversion = `${argsDotVariable}.map((e) => Spanner.float(e))`; } } else if (typeDefinition.kind === "Message") { this.sqlContentBuilder.importFromMessageSerializer("serializeMessage"); let tsTypeDescriptor = (0, util_1.toUppercaseSnaked)(typeDefinition.name); this.sqlContentBuilder.importFromDefinition(columnType.import, tsTypeDescriptor); if (!columnType.isArray) { tsType = typeDefinition.name; queryType = `{ type: "bytes" }`; conversion = `Buffer.from(serializeMessage(${argsDotVariable}, ${tsTypeDescriptor}).buffer)`; } else { tsType = `Array<${typeDefinition.name}>`; queryType = `{ type: "array", child: { type: "bytes" } }`; conversion = `${argsDotVariable}.map((e) => Buffer.from(serializeMessage(e, ${tsTypeDescriptor}).buffer))`; } } } else { if (!columnType.isArray) { queryType = `{ type: "${COLUMN_PRIMITIVE_TYPE_TO_QUERY_TYPE.get(columnType.type)}" }`; switch (columnType.type) { case "int53": conversion = `${argsDotVariable}.toString()`; break; case "float64": this.sqlContentBuilder.importFromSpanner("Spanner"); conversion = `Spanner.float(${argsDotVariable})`; break; case "timestamp": conversion = `new Date(${argsDotVariable}).toISOString()`; break; default: // bool, string conversion = `${argsDotVariable}`; } } else { queryType = `{ type: "array", child: { type: "${COLUMN_PRIMITIVE_TYPE_TO_QUERY_TYPE.get(columnType.type)}" } }`; tsType = `Array<${tsType}>`; switch (columnType.type) { case "int53": conversion = `${argsDotVariable}.map((e) => e.toString())`; break; case "float64": this.sqlContentBuilder.importFromSpanner("Spanner"); conversion = `${argsDotVariable}.map((e) => Spanner.float(e))`; break; case "timestamp": conversion = `${argsDotVariable}.map((e) => new Date(e).toISOString())`; break; default: // bool, string conversion = `${argsDotVariable}`; } } } this.inputQueryTypes.push(`${argVariable}: ${queryType}`); if (columnType.nullable) { this.inputArgs.push(`${argVariable}?: ${tsType}`); this.inputConversions.push(`${argVariable}: ${argsDotVariable} == null ? null : ${conversion}`); } else { this.inputArgs.push(`${argVariable}: ${tsType}`); this.inputConversions.push(`${argVariable}: ${conversion}`); } } clearOutput(