123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717 |
- const Promise = require('bluebird');
- const Selection = require('../select/Selection');
- const { uniqBy, values } = require('../../../utils/objectUtils');
- const { Type: ValidationErrorType } = require('../../../model/ValidationError');
- const ID_LENGTH_LIMIT = 63;
- const RELATION_RECURSION_LIMIT = 64;
- class RelationJoinBuilder {
- constructor({ modelClass, expression, filters = Object.create(null) }) {
- this.rootModelClass = modelClass;
- this.expression = expression;
- this.filters = filters;
- this.allRelations = null;
- this.pathInfo = new Map();
- this.encodings = new Map();
- this.decodings = new Map();
- this.encIdx = 0;
- this.opt = {
- minimize: false,
- separator: ':',
- aliases: {}
- };
- }
- setOptions(opt) {
- this.opt = Object.assign(this.opt, opt);
- }
- /**
- * Fetches the column information needed for building the select clauses.
- * This must be called before calling `build`. `buildJoinOnly` can be called
- * without this since it doesn't build selects.
- */
- fetchColumnInfo(builder) {
- const allModelClasses = findAllModels(this.expression, this.rootModelClass);
- return Promise.map(
- allModelClasses,
- ModelClass => ModelClass.fetchTableMetadata({ parentBuilder: builder }),
- {
- concurrency: this.rootModelClass.concurrency
- }
- );
- }
- buildJoinOnly(builder) {
- this.doBuild({
- expr: this.expression,
- builder,
- modelClass: builder.modelClass(),
- joinOperation: this.opt.joinOperation || 'leftJoin',
- parentInfo: null,
- relation: null,
- noSelects: true,
- path: ''
- });
- }
- build(builder) {
- const tableName = builder.tableNameFor(this.rootModelClass);
- const tableAlias = builder.tableRefFor(this.rootModelClass);
- if (tableName === tableAlias) {
- builder.table(tableName);
- } else {
- builder.table(`${tableName} as ${tableAlias}`);
- }
- this.doBuild({
- expr: this.expression,
- builder,
- modelClass: builder.modelClass(),
- joinOperation: this.opt.joinOperation || 'leftJoin',
- selectFilterQuery: builder.clone(),
- parentInfo: null,
- relation: null,
- path: ''
- });
- }
- rowsToTree(rows) {
- if (!Array.isArray(rows) || rows.length === 0) {
- return rows;
- }
- const keyInfoByPath = this.createKeyInfo(rows);
- const pathInfo = Array.from(this.pathInfo.values());
- const tree = Object.create(null);
- const stack = new Map();
- for (let i = 0, lr = rows.length; i < lr; ++i) {
- const row = rows[i];
- let curBranch = tree;
- for (let j = 0, lp = pathInfo.length; j < lp; ++j) {
- const pInfo = pathInfo[j];
- const id = pInfo.idGetter(row);
- let model;
- if (id === null) {
- continue;
- }
- if (pInfo.relation) {
- const parentModel = stack.get(pInfo.encParentPath);
- curBranch = pInfo.getBranch(parentModel);
- if (!curBranch) {
- curBranch = pInfo.createBranch(parentModel);
- }
- }
- model = pInfo.getModelFromBranch(curBranch, id);
- if (!model) {
- model = createModel(row, pInfo, keyInfoByPath);
- pInfo.setModelToBranch(curBranch, id, model);
- }
- stack.set(pInfo.encPath, model);
- }
- }
- return this.finalize(pathInfo[0], values(tree));
- }
- createKeyInfo(rows) {
- const keys = Object.keys(rows[0]);
- const keyInfo = new Map();
- for (let i = 0, l = keys.length; i < l; ++i) {
- const key = keys[i];
- const sepIdx = key.lastIndexOf(this.sep);
- let col, pInfo;
- if (sepIdx === -1) {
- pInfo = this.pathInfo.get('');
- col = key;
- } else {
- const encPath = key.substr(0, sepIdx);
- const path = this.decode(encPath);
- col = key.substr(sepIdx + this.sep.length);
- pInfo = this.pathInfo.get(path);
- }
- if (!pInfo.omitCols.has(col)) {
- let infoArr = keyInfo.get(pInfo.encPath);
- if (!infoArr) {
- infoArr = [];
- keyInfo.set(pInfo.encPath, infoArr);
- }
- infoArr.push({ pInfo, key, col });
- }
- }
- return keyInfo;
- }
- finalize(pInfo, models) {
- const relNames = Array.from(pInfo.children.keys());
- if (Array.isArray(models)) {
- for (let m = 0, lm = models.length; m < lm; ++m) {
- this.finalizeOne(pInfo, relNames, models[m]);
- }
- } else if (models) {
- this.finalizeOne(pInfo, relNames, models);
- }
- return models;
- }
- finalizeOne(pInfo, relNames, model) {
- for (let r = 0, lr = relNames.length; r < lr; ++r) {
- const relName = relNames[r];
- const branch = model[relName];
- const childPathInfo = pInfo.children.get(relName);
- const finalized = childPathInfo.finalizeBranch(branch, model);
- this.finalize(childPathInfo, finalized);
- }
- }
- doBuild({
- expr,
- builder,
- relation,
- modelClass,
- selectFilterQuery,
- joinOperation,
- parentInfo,
- noSelects,
- path
- }) {
- if (!this.allRelations) {
- this.allRelations = findAllRelations(this.expression, this.rootModelClass);
- }
- const info = this.createPathInfo({
- modelClass,
- path,
- expr,
- relation,
- parentInfo
- });
- this.pathInfo.set(path, info);
- if (!noSelects) {
- this.buildSelects({
- builder,
- selectFilterQuery,
- modelClass,
- relation,
- info
- });
- }
- forEachExpr(expr, modelClass, (childExpr, relation) => {
- const nextPath = this.joinPath(path, childExpr.$name);
- const encNextPath = this.encode(nextPath);
- const encJoinTablePath = relation.joinTable ? this.encode(joinTableForPath(nextPath)) : null;
- const ownerTable = info.encPath || undefined;
- const filterQuery = createFilterQuery({
- builder,
- modelClass,
- relation,
- expr: childExpr,
- filters: this.filters
- });
- const relatedJoinSelectQuery = createRelatedJoinFromQuery({
- filterQuery,
- relation,
- allRelations: this.allRelations
- });
- relation.join(builder, {
- ownerTable,
- joinOperation,
- relatedTableAlias: encNextPath,
- joinTableAlias: encJoinTablePath,
- relatedJoinSelectQuery
- });
- // Apply relation.modify since it may also contains selections. Don't move this
- // to the createFilterQuery function because relatedJoinSelectQuery is cloned
- // from the return value of that function and we don't want relation.modify
- // to be called twice for it.
- filterQuery.modify(relation.modify);
- this.doBuild({
- expr: childExpr,
- builder,
- modelClass: relation.relatedModelClass,
- joinOperation,
- relation,
- parentInfo: info,
- noSelects,
- path: nextPath,
- selectFilterQuery: filterQuery
- });
- });
- }
- createPathInfo({ modelClass, path, expr, relation, parentInfo }) {
- const encPath = this.encode(path);
- let info;
- if (relation && relation.isOneToOne()) {
- info = new OneToOnePathInfo();
- } else {
- info = new PathInfo();
- }
- info.path = path;
- info.encPath = encPath;
- info.parentPath = parentInfo && parentInfo.path;
- info.encParentPath = parentInfo && parentInfo.encPath;
- info.modelClass = modelClass;
- info.relation = relation;
- info.idGetter = this.createIdGetter(modelClass, encPath);
- info.relationAlias = expr.$name;
- if (parentInfo) {
- parentInfo.children.set(expr.$name, info);
- }
- return info;
- }
- buildSelects({ builder, selectFilterQuery, modelClass, relation, info }) {
- const selects = [];
- const idCols = modelClass.getIdColumnArray();
- const rootTable = builder.tableRefFor(this.rootModelClass);
- const isSelectFilterQuerySubQuery = !!info.encPath;
- let selections = selectFilterQuery.findAllSelections();
- const selectAllIndex = selections.findIndex(isSelectAll);
- // If there are no explicit selects, or there is a `select *` item,
- // we need to select all columns using the schema information
- // in `modelClass.tableMetadata()`.
- if (selections.length === 0 || selectAllIndex !== -1) {
- const table = builder.tableNameFor(modelClass);
- selections.splice(selectAllIndex, 1);
- selections = modelClass
- .tableMetadata({ table })
- .columns.map(it => new Selection(null, it))
- .concat(selections);
- }
- // Id columns always need to be selected so that we are able to construct
- // the tree structure from the flat columns.
- for (let i = 0, l = idCols.length; i < l; ++i) {
- const idCol = idCols[i];
- if (!selections.some(it => it.name === idCol)) {
- info.omitCols.add(idCol);
- selections.unshift(new Selection(null, idCol));
- }
- }
- for (let i = 0, l = selections.length; i < l; ++i) {
- const selection = selections[i];
- // If `selections` come from a subquery, we need to use the possible alias instead
- // of the column name because that's what the root query sees instead of the real
- // column name.
- const col = isSelectFilterQuerySubQuery ? selection.name : selection.column;
- const name = selection.name;
- const fullCol = `${info.encPath || rootTable}.${col}`;
- const alias = this.joinPath(info.encPath, name);
- if (!builder.hasSelectionAs(fullCol, alias, true)) {
- checkAliasLength(modelClass, alias);
- selects.push(`${fullCol} as ${alias}`);
- }
- }
- if (relation && relation.joinTableExtras) {
- const joinTable = this.encode(joinTableForPath(info.path));
- for (let i = 0, l = relation.joinTableExtras.length; i < l; ++i) {
- const extra = relation.joinTableExtras[i];
- const filterPassed = selectFilterQuery.hasSelection(extra.joinTableCol);
- if (filterPassed) {
- const fullCol = `${joinTable}.${extra.joinTableCol}`;
- if (!builder.hasSelection(fullCol, true)) {
- const alias = this.joinPath(info.encPath, extra.aliasCol);
- checkAliasLength(modelClass, alias);
- selects.push(`${fullCol} as ${alias}`);
- }
- }
- }
- }
- builder.select(selects);
- }
- encode(path) {
- if (!this.opt.minimize) {
- let encPath = this.encodings.get(path);
- if (!encPath) {
- const parts = path.split(this.sep);
- // Don't encode the root.
- if (!path) {
- encPath = path;
- } else {
- encPath = parts.map(part => this.opt.aliases[part] || part).join(this.sep);
- }
- this.encodings.set(path, encPath);
- this.decodings.set(encPath, path);
- }
- return encPath;
- } else {
- let encPath = this.encodings.get(path);
- if (!encPath) {
- // Don't encode the root.
- if (!path) {
- encPath = path;
- } else {
- encPath = this.nextEncodedPath();
- }
- this.encodings.set(path, encPath);
- this.decodings.set(encPath, path);
- }
- return encPath;
- }
- }
- decode(path) {
- return this.decodings.get(path);
- }
- nextEncodedPath() {
- return `_t${++this.encIdx}`;
- }
- createIdGetter(modelClass, path) {
- const idCols = modelClass.getIdColumnArray().map(col => this.joinPath(path, col));
- if (idCols.length === 1) {
- return createSingleIdGetter(idCols);
- } else if (idCols.length === 2) {
- return createTwoIdGetter(idCols);
- } else if (idCols.length === 3) {
- return createThreeIdGetter(idCols);
- } else {
- return createNIdGetter(idCols);
- }
- }
- get sep() {
- return this.opt.separator;
- }
- joinPath(path, nextPart) {
- if (path) {
- return `${path}${this.sep}${nextPart}`;
- } else {
- return nextPart;
- }
- }
- }
- function findAllModels(expr, modelClass) {
- const modelClasses = [];
- findAllModelsImpl(expr, modelClass, modelClasses);
- return uniqBy(modelClasses, getTableName);
- }
- function getTableName(modelClass) {
- return modelClass.getTableName();
- }
- function findAllModelsImpl(expr, modelClass, models) {
- models.push(modelClass);
- forEachExpr(expr, modelClass, (childExpr, relation) => {
- findAllModelsImpl(childExpr, relation.relatedModelClass, models);
- });
- }
- function findAllRelations(expr, modelClass) {
- const relations = [];
- findAllRelationsImpl(expr, modelClass, relations);
- return uniqBy(relations);
- }
- function strictEqual(lhs, rhs) {
- return lhs === rhs;
- }
- function findAllRelationsImpl(expr, modelClass, relations) {
- forEachExpr(expr, modelClass, (childExpr, relation) => {
- relations.push(relation);
- findAllRelationsImpl(childExpr, relation.relatedModelClass, relations);
- });
- }
- function forEachExpr(expr, modelClass, callback) {
- const relations = modelClass.getRelations();
- if (expr.isAllRecursive || expr.maxRecursionDepth > RELATION_RECURSION_LIMIT) {
- throw modelClass.createValidationError({
- type: ValidationErrorType.RelationExpression,
- message: `recursion depth of eager expression ${expr.toString()} too big for JoinEagerAlgorithm`
- });
- }
- expr.forEachChildExpression(relations, callback);
- }
- function createSingleIdGetter(idCols) {
- const idCol = idCols[0];
- return row => {
- const val = row[idCol];
- if (isNullOrUndefined(val)) {
- return null;
- } else {
- return `id:${val}`;
- }
- };
- }
- function createTwoIdGetter(idCols) {
- const idCol1 = idCols[0];
- const idCol2 = idCols[1];
- return row => {
- const val1 = row[idCol1];
- const val2 = row[idCol2];
- if (isNullOrUndefined(val1) || isNullOrUndefined(val2)) {
- return null;
- } else {
- return `id:${val1},${val2}`;
- }
- };
- }
- function createThreeIdGetter(idCols) {
- const idCol1 = idCols[0];
- const idCol2 = idCols[1];
- const idCol3 = idCols[2];
- return row => {
- const val1 = row[idCol1];
- const val2 = row[idCol2];
- const val3 = row[idCol3];
- if (isNullOrUndefined(val1) || isNullOrUndefined(val2) || isNullOrUndefined(val3)) {
- return null;
- } else {
- return `id:${val1},${val2},${val3}`;
- }
- };
- }
- function createNIdGetter(idCols) {
- return row => {
- let id = 'id:';
- for (let i = 0, l = idCols.length; i < l; ++i) {
- const val = row[idCols[i]];
- if (isNullOrUndefined(val)) {
- return null;
- }
- id += (i > 0 ? ',' : '') + val;
- }
- return id;
- };
- }
- function isNullOrUndefined(val) {
- return val === null || val === undefined;
- }
- function createFilterQuery({ builder, modelClass, expr, filters, relation }) {
- const modelNamedFilters = relation.relatedModelClass.namedFilters || {};
- const filterQuery = relation.relatedModelClass.query().childQueryOf(builder);
- for (let i = 0, l = expr.$modify.length; i < l; ++i) {
- const filterName = expr.$modify[i];
- const filter = filters[filterName] || modelNamedFilters[filterName];
- if (typeof filter !== 'function') {
- throw modelClass.createValidationError({
- type: ValidationErrorType.RelationExpression,
- message: `could not find filter "${filterName}" for relation "${relation.name}"`
- });
- }
- filter(filterQuery);
- }
- return filterQuery;
- }
- function createRelatedJoinFromQuery({ filterQuery, relation, allRelations }) {
- const relatedJoinFromQuery = filterQuery.clone();
- const tableRef = filterQuery.tableRefFor(relation.relatedModelClass);
- const allForeignKeys = findAllForeignKeysForModel({
- modelClass: relation.relatedModelClass,
- allRelations
- });
- return relatedJoinFromQuery.select(
- allForeignKeys
- .filter(col => {
- return !relatedJoinFromQuery.hasSelectionAs(col, col);
- })
- .map(col => {
- return `${tableRef}.${col}`;
- })
- );
- }
- function findAllForeignKeysForModel({ modelClass, allRelations }) {
- const foreignKeys = modelClass.getIdColumnArray().slice();
- allRelations.forEach(rel => {
- if (rel.relatedModelClass === modelClass) {
- rel.relatedProp.cols.forEach(col => foreignKeys.push(col));
- }
- if (rel.ownerModelClass === modelClass) {
- rel.ownerProp.cols.forEach(col => foreignKeys.push(col));
- }
- });
- return uniqBy(foreignKeys);
- }
- function createModel(row, pInfo, keyInfoByPath) {
- const keyInfo = keyInfoByPath.get(pInfo.encPath);
- const json = {};
- for (let k = 0, lk = keyInfo.length; k < lk; ++k) {
- const kInfo = keyInfo[k];
- json[kInfo.col] = row[kInfo.key];
- }
- return pInfo.modelClass.fromDatabaseJson(json);
- }
- function joinTableForPath(path) {
- return path + '_join';
- }
- function checkAliasLength(modelClass, alias) {
- if (alias.length > ID_LENGTH_LIMIT) {
- throw modelClass.createValidationError({
- type: ValidationErrorType.RelationExpression,
- message: `identifier ${alias} is over ${ID_LENGTH_LIMIT} characters long and would be truncated by the database engine.`
- });
- }
- }
- function isSelectAll(selection) {
- return selection.column === '*';
- }
- class PathInfo {
- constructor() {
- this.path = null;
- this.encPath = null;
- this.encParentPath = null;
- this.modelClass = null;
- this.relation = null;
- this.omitCols = new Set();
- this.children = new Map();
- this.idGetter = null;
- this.relationAlias = null;
- }
- createBranch(parentModel) {
- const branch = Object.create(null);
- parentModel[this.relationAlias] = branch;
- return branch;
- }
- getBranch(parentModel) {
- return parentModel[this.relationAlias];
- }
- getModelFromBranch(branch, id) {
- return branch[id];
- }
- setModelToBranch(branch, id, model) {
- branch[id] = model;
- }
- finalizeBranch(branch, parentModel) {
- const relModels = values(branch);
- parentModel[this.relationAlias] = relModels;
- return relModels;
- }
- }
- class OneToOnePathInfo extends PathInfo {
- createBranch(parentModel) {
- return parentModel;
- }
- getBranch(parentModel) {
- return parentModel;
- }
- getModelFromBranch(branch, id) {
- return branch[this.relationAlias];
- }
- setModelToBranch(branch, id, model) {
- branch[this.relationAlias] = model;
- }
- finalizeBranch(branch, parentModel) {
- parentModel[this.relationAlias] = branch || null;
- return branch || null;
- }
- }
- module.exports = RelationJoinBuilder;
|