convert-join-criteria.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. // ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██████╗ ████████╗ ██╗ ██████╗ ██╗███╗ ██╗
  2. // ██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝██╔══██╗╚══██╔══╝ ██║██╔═══██╗██║████╗ ██║
  3. // ██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝ ██║ ██║██║ ██║██║██╔██╗ ██║
  4. // ██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ██║ ██ ██║██║ ██║██║██║╚██╗██║
  5. // ╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ██║ ██║ ╚█████╔╝╚██████╔╝██║██║ ╚████║
  6. // ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝
  7. //
  8. // ██████╗██████╗ ██╗████████╗███████╗██████╗ ██╗ █████╗
  9. // ██╔════╝██╔══██╗██║╚══██╔══╝██╔════╝██╔══██╗██║██╔══██╗
  10. // ██║ ██████╔╝██║ ██║ █████╗ ██████╔╝██║███████║
  11. // ██║ ██╔══██╗██║ ██║ ██╔══╝ ██╔══██╗██║██╔══██║
  12. // ╚██████╗██║ ██║██║ ██║ ███████╗██║ ██║██║██║ ██║
  13. // ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
  14. //
  15. // Given some Waterline criteria, inspect it for any joins and determine how
  16. // to go about building up queries. If the joins don't contain any criteria
  17. // or any skip, sort, or limit clauses then a single query can be built.
  18. // Otherwise the first query will need to be run and then using the primary
  19. // key of the "parent" build up a child query. This child query will be either
  20. // an IN query using a map of the parent's primary key or a big UNION query.
  21. // The UNION query is used in situations where you are basically filtering
  22. // the child results. It's a rare case and will result in a non-ideal query
  23. // but is supported in the Waterline API.
  24. //
  25. // EX: In the following case the UNION query will run the query specific to
  26. // each user that is found.`
  27. //
  28. // Model.find()
  29. // .populate('pets', { type: 'cat', sort: 'name', limit: 5 })
  30. // .exec()
  31. //
  32. var _ = require('@sailshq/lodash');
  33. var Helpers = require('./private');
  34. var Converter = require('../query/converter');
  35. module.exports = function convertCriteria(options) {
  36. // ╦ ╦╔═╗╦ ╦╔╦╗╔═╗╔╦╗╔═╗ ┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐
  37. // ╚╗╔╝╠═╣║ ║ ║║╠═╣ ║ ║╣ │ │├─┘ │ ││ ││││└─┐
  38. // ╚╝ ╩ ╩╩═╝╩═╩╝╩ ╩ ╩ ╚═╝ └─┘┴ ┴ ┴└─┘┘└┘└─┘
  39. if (_.isUndefined(options) || !_.isPlainObject(options)) {
  40. throw new Error('Invalid options argument. Options must contain: tableName, schemaName, getPk, and criteria.');
  41. }
  42. if (!_.has(options, 'query') || !_.isPlainObject(options.query)) {
  43. throw new Error('Invalid option used in options argument. Missing or invalid query.');
  44. }
  45. if (!_.has(options, 'getPk') || !_.isFunction(options.getPk)) {
  46. throw new Error('Invalid option used in options argument. Missing or invalid getPk function.');
  47. }
  48. // Store the validated options for use
  49. var query = options.query;
  50. var schemaName = options.schemaName;
  51. var getPk = options.getPk;
  52. // Add a statement var that will be used to build up a Waterline Statement
  53. // from the criteria.
  54. var parentStatement;
  55. var childStatements = [];
  56. // Add a flag to determine if this query will need to be a slow join or not.
  57. var slowJoin = false;
  58. // ╔╗╔╔═╗ ┬┌─┐┬┌┐┌┌─┐
  59. // ║║║║ ║ ││ │││││└─┐
  60. // ╝╚╝╚═╝ └┘└─┘┴┘└┘└─┘
  61. // If the criteria has no join instructions go ahead and build a very simple
  62. // statement then bail out. Nothing fancy to do here.
  63. if (!_.has(query, 'joins')) {
  64. try {
  65. (function buildUpConvertCriteria() {
  66. var convertObj = {
  67. model: query.using,
  68. method: 'find',
  69. criteria: query.criteria
  70. };
  71. if (schemaName) {
  72. convertObj.opts = {
  73. schema: schemaName
  74. };
  75. }
  76. parentStatement = Converter(convertObj);
  77. })();
  78. } catch (e) {
  79. throw new Error('There was an error converting the Waterline Query into a Waterline Statement: ' + e.message);
  80. }
  81. return {
  82. parentStatement: parentStatement
  83. };
  84. }
  85. // ╔═╗╦ ╔═╗╔╗╔ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
  86. // ╠═╝║ ╠═╣║║║ │─┼┐│ │├┤ ├┬┘└┬┘
  87. // ╩ ╩═╝╩ ╩╝╚╝ └─┘└└─┘└─┘┴└─ ┴
  88. // If there ARE joins, replace the criteria with the planned instructions. These
  89. // are instructions that have been expanded to include the normalized join
  90. // strategy.
  91. var instructions = Helpers.planner({
  92. joins: query.joins,
  93. getPk: getPk
  94. });
  95. // ╔═╗╦ ╦╔═╗╔═╗╦╔═ ┌─┐┌─┐┬─┐ ┌─┐┬ ┌─┐┬ ┬ ┬┌─┐┬┌┐┌
  96. // ║ ╠═╣║╣ ║ ╠╩╗ ├┤ │ │├┬┘ └─┐│ │ ││││ ││ │││││
  97. // ╚═╝╩ ╩╚═╝╚═╝╩ ╩ └ └─┘┴└─ └─┘┴─┘└─┘└┴┘ └┘└─┘┴┘└┘
  98. // Go through and check if any of the join instructions are
  99. // using any sort of criteria. If not, build up a single statement.
  100. //
  101. // When criteria is used on a join (populate) it complicates things. Based on
  102. // the way populates work in Waterline, criteria on the population is used
  103. // as a filter on the children and not the parents. Because of this the criteria
  104. // can't simply be added into the query. This is called a slow join because it
  105. // can't be fulfilled in a single query, it must be run in two queries. The
  106. // first query finds all the matching parent records and the second query finds
  107. // the records being populated along with the given criteria.
  108. // Hold an array of population aliases that can't be run in a single query
  109. var slowJoinAliases = [];
  110. // Hold a map of joins that will be needed. If two joins need the same data
  111. // but are connected in different ways then there will be some slow joins
  112. // needed. This is used when you have a model that has multiple collection
  113. // attributes pointing to the same model but using different `via` attributes.
  114. // See below for more information.
  115. var joinMaps = {};
  116. // Hold the maximum integer size
  117. var maxInt = Number.MAX_SAFE_INTEGER || 9007199254740991;
  118. _.each(instructions, function processJoins(val, key) {
  119. // If the join has a type 1 strategy it will never be a slow join
  120. if (val.strategy && val.strategy.strategy === 1) {
  121. return;
  122. }
  123. // Process each instruction for the aliases being populated
  124. _.each(val.instructions, function checkForCriteria(joinSet) {
  125. // ╔═╗╦ ╦╔═╗╔═╗╦╔═ ┌─┐┌─┐┬─┐ ┌─┐┌─┐┬─┐┌─┐┌┐┌┌┬┐ ┌─┐┬ ┌─┐┬ ┬ ┬┌─┐┬┌┐┌
  126. // ║ ╠═╣║╣ ║ ╠╩╗ ├┤ │ │├┬┘ ├─┘├─┤├┬┘├┤ │││ │ └─┐│ │ ││││ ││ │││││
  127. // ╚═╝╩ ╩╚═╝╚═╝╩ ╩ └ └─┘┴└─ ┴ ┴ ┴┴└─└─┘┘└┘ ┴ └─┘┴─┘└─┘└┴┘ └┘└─┘┴┘└┘
  128. // If the parent criteria contains a SKIP or LIMIT then all the populates in
  129. // the query will need to be slow joins.
  130. if (_.has(query.criteria, 'skip') && query.criteria.skip > 0) {
  131. slowJoin = true;
  132. slowJoinAliases.push(key);
  133. return;
  134. }
  135. if (_.has(query.criteria, 'limit') && query.criteria.limit < maxInt) {
  136. slowJoin = true;
  137. slowJoinAliases.push(key);
  138. return;
  139. }
  140. // Check if the tables in this have already been joined in some way. If
  141. // so, then a slow join is needed to fufill any further requests. This is
  142. // commonly used when a parent is populating multiple attributes from the
  143. // same table. See the multiple foreign keys test from Waterline-Adapter-Tests.
  144. if (_.has(joinMaps, joinSet.child) && joinMaps[joinSet.child] !== joinSet.childKey) {
  145. slowJoin = true;
  146. slowJoinAliases.push(key);
  147. return;
  148. }
  149. // Add this join to the mapping
  150. joinMaps[joinSet.child] = joinSet.childKey;
  151. // If there isn't any criteria set there is no need to make this a slowJoin
  152. if (!_.has(joinSet, 'criteria')) {
  153. return;
  154. }
  155. // If there is an empty criteria object set, no need to make this a slowJoin
  156. if (_.keys(joinSet.criteria).length === 0) {
  157. return;
  158. }
  159. // Check for SLOW JOIN criteria keys (skip, limit, and where) and make
  160. // sure if they are set that they aren't the "base" values set by Waterline.
  161. if (_.has(joinSet.criteria, 'limit')) {
  162. if (joinSet.criteria.limit === maxInt) {
  163. delete joinSet.criteria.limit;
  164. }
  165. }
  166. if (_.has(joinSet.criteria, 'skip')) {
  167. if (joinSet.criteria.skip === 0) {
  168. delete joinSet.criteria.skip;
  169. }
  170. }
  171. if (_.has(joinSet.criteria, 'where')) {
  172. if (_.keys(joinSet.criteria.where).length === 0) {
  173. delete joinSet.criteria.where;
  174. }
  175. }
  176. // If there are still skip and limits attached, this is a slow join
  177. if (_.has(joinSet.criteria, 'sort') || _.has(joinSet.criteria, 'limit') || _.has(joinSet.criteria, 'skip') || _.has(joinSet.criteria, 'where')) {
  178. slowJoin = true;
  179. slowJoinAliases.push(key);
  180. }
  181. });
  182. });
  183. // Ensure that the slowJoinAliases array is made up of unique aliases
  184. slowJoinAliases = _.uniq(slowJoinAliases);
  185. // ╔╗ ╦ ╦╦╦ ╔╦╗ ┬┌─┐┬┌┐┌ ┌─┐┌┬┐┌─┐┌┬┐┌─┐┌┬┐┌─┐┌┐┌┌┬┐
  186. // ╠╩╗║ ║║║ ║║ ││ │││││ └─┐ │ ├─┤ │ ├┤ │││├┤ │││ │
  187. // ╚═╝╚═╝╩╩═╝═╩╝ └┘└─┘┴┘└┘ └─┘ ┴ ┴ ┴ ┴ └─┘┴ ┴└─┘┘└┘ ┴
  188. // If there wasn't a slow join found go ahead and try and build a statement.
  189. // This is a query that can be executed in a single run. These will be the
  190. // fastest and take the least amount of time to run.
  191. if (!slowJoin) {
  192. var instructionArray = _.map(_.keys(instructions), function parseAlias(alias) {
  193. return instructions[alias];
  194. });
  195. // Try and convert the criteria into a Waterline Statement
  196. try {
  197. (function buildUpConvertCriteria() {
  198. var convertObj = {
  199. model: query.using,
  200. method: 'find',
  201. criteria: query.criteria,
  202. joins: instructionArray,
  203. };
  204. if (schemaName) {
  205. convertObj.opts = {
  206. schema: schemaName
  207. };
  208. }
  209. parentStatement = Converter(convertObj);
  210. })();
  211. } catch (e) {
  212. throw new Error('There was an error converting the Waterline Query into a Waterline Statement: ' + e.message);
  213. }
  214. // After check if this is a type 3 join. The VIA_JUNCTOR queries need a way
  215. // to link parent and child records together without holding all the additional
  216. // join table records in memory. To do this we add a special SELECT statement
  217. // to the criteria instructions. This allows the child records to appear
  218. // as if they were simple hasMany records.
  219. _.each(instructionArray, function checkStrategy(val) {
  220. if (val.strategy.strategy !== 3) {
  221. return;
  222. }
  223. // Otherwise modify the SELECT and add a special key
  224. var junctor = _.first(val.instructions);
  225. var child = _.last(val.instructions);
  226. // The "special" key is simply a reserved word `__parent_fk` that can easily
  227. // be parsed out of the results. It contains the value that was found in
  228. // the join table that links it to the parent.
  229. var selectStr = junctor.childAlias + '.' + junctor.childKey + ' as ' + child.alias + '___parent_fk';
  230. parentStatement.select.push(selectStr);
  231. });
  232. // Expand the criteria so it doesn't contain any ambiguous fields
  233. try {
  234. parentStatement.where = Helpers.expandCriteria(parentStatement.where, query.using);
  235. } catch (e) {
  236. throw new Error('There was an error trying to expand the criteria used in the WHERE clause. Perhaps it is invalid? ' + e.stack);
  237. }
  238. return {
  239. parentStatement: parentStatement,
  240. instructions: instructions
  241. };
  242. }
  243. // ╔╗ ╦ ╦╦╦ ╔╦╗ ┌─┐┬ ┌─┐┬ ┬ ┌─┐┌─┐┬─┐┌─┐┌┐┌┌┬┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
  244. // ╠╩╗║ ║║║ ║║ └─┐│ │ ││││ ├─┘├─┤├┬┘├┤ │││ │ │─┼┐│ │├┤ ├┬┘└┬┘
  245. // ╚═╝╚═╝╩╩═╝═╩╝ └─┘┴─┘└─┘└┴┘ ┴ ┴ ┴┴└─└─┘┘└┘ ┴ └─┘└└─┘└─┘┴└─ ┴
  246. // Otherwise build up a statement for the "parent" query. This is just a
  247. // statement with all the join instructions stripped out from it.
  248. //
  249. // It's responsibility is to get the parent's primary keys that can be used
  250. // in another query to fufill the request. These are much slower.
  251. var fastJoinInstructionArray;
  252. var fastInstructions = _.extend({}, instructions);
  253. try {
  254. // Take the instructions and remove any aliases that need to be run using a
  255. // slow join. Whatever is left should be able to be run in a single query.
  256. _.each(slowJoinAliases, function removeSlowJoinAliases(alias) {
  257. delete fastInstructions[alias];
  258. });
  259. // Normalize the instructions array
  260. fastJoinInstructionArray = _.map(_.keys(fastInstructions), function parseAlias(alias) {
  261. return instructions[alias];
  262. });
  263. (function buildUpConvertCriteria() {
  264. var convertObj = {
  265. model: query.using,
  266. method: 'find',
  267. criteria: query.criteria,
  268. joins: fastJoinInstructionArray
  269. };
  270. if (schemaName) {
  271. convertObj.opts = {
  272. schema: schemaName
  273. };
  274. }
  275. parentStatement = Converter(convertObj);
  276. })();
  277. } catch (e) {
  278. throw new Error('There was an error converting the Waterline Query into a Waterline Statement. ' + e.stack);
  279. }
  280. // After check if this is a type 3 join. The VIA_JUNCTOR queries need a way
  281. // to link parent and child records together without holding all the additional
  282. // join table records in memory. To do this we add a special SELECT statement
  283. // to the criteria instructions. This allows the child records to appear
  284. // as if they were simple hasMany records.
  285. _.each(fastJoinInstructionArray, function checkStrategy(val) {
  286. if (val.strategy.strategy !== 3) {
  287. return;
  288. }
  289. // Otherwise modify the SELECT and add a special key
  290. var junctor = _.first(val.instructions);
  291. var child = _.last(val.instructions);
  292. // The "special" key is simply a reserved word `__parent_fk` that can easily
  293. // be parsed out of the results. It contains the value that was found in
  294. // the join table that links it to the parent.
  295. var selectStr = junctor.childAlias + '.' + junctor.childKey + ' as ' + child.alias + '___parent_fk';
  296. parentStatement.select.push(selectStr);
  297. });
  298. // Expand the criteria so it doesn't contain any ambiguous fields
  299. try {
  300. parentStatement.where = Helpers.expandCriteria(parentStatement.where, query.using);
  301. } catch (e) {
  302. throw new Error('There was an error trying to expand the criteria used in the WHERE clause. Perhaps it is invalid? ' + e.stack);
  303. }
  304. // ╔╗ ╦ ╦╦╦ ╔╦╗ ┌─┐┬ ┌─┐┬ ┬ ┌─┐┬ ┬┬┬ ┌┬┐ ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬
  305. // ╠╩╗║ ║║║ ║║ └─┐│ │ ││││ │ ├─┤││ ││ │─┼┐│ │├┤ ├┬┘└┬┘
  306. // ╚═╝╚═╝╩╩═╝═╩╝ └─┘┴─┘└─┘└┴┘ └─┘┴ ┴┴┴─┘─┴┘ └─┘└└─┘└─┘┴└─ ┴
  307. // ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐
  308. // │ ├┤ │││├─┘│ ├─┤ │ ├┤
  309. // ┴ └─┘┴ ┴┴ ┴─┘┴ ┴ ┴ └─┘
  310. // This is a template that will be used for the children queries. It will be
  311. // formed based upon the type of query being run and the strategy used.
  312. //
  313. // The template is simply a placeholder that represents what query will be need
  314. // to be run to find the child. It contains a placeholder value that can't be
  315. // generated until the parent query has finished.
  316. //
  317. // Once a parent query has been run the child template can be rendered and then
  318. // run through as a native query to get the remaining results.
  319. _.each(slowJoinAliases, function buildJoinTemplate(alias) {
  320. // Grab the join instructions
  321. var _instructions = instructions[alias];
  322. // Grab the strategy type off the instructions
  323. var strategy = _instructions.strategy.strategy;
  324. // Hold the generated statement template
  325. var statement;
  326. // Hold the primary key attribute to use for the template
  327. var primaryKeyAttr;
  328. // Hold an empty template for the where criteria that will be built as a
  329. // stand in. This will be editied to contain the primary keys of the parent
  330. // query results.
  331. var whereTemplate = {};
  332. // Grab the parent instructions
  333. var parentInstructions = _.first(_instructions.instructions);
  334. // Clean up any default join criteria that would have a bearing on how the
  335. // query gets built.
  336. if (_.has(parentInstructions.criteria, 'skip') && parentInstructions.criteria.skip === 0) {
  337. delete parentInstructions.criteria.skip;
  338. }
  339. if (_.has(parentInstructions.criteria, 'limit') && parentInstructions.criteria.limit === maxInt) {
  340. delete parentInstructions.criteria.limit;
  341. }
  342. // Check if the child result will be paginated
  343. var paginated = _.get(parentInstructions.criteria, 'skip', 0) !== 0 || _.get(parentInstructions.criteria, 'limit', Number.MAX_SAFE_INTEGER) !== Number.MAX_SAFE_INTEGER;
  344. // ╔═╗╔═╗╔╗╔╔═╗╦═╗╔═╗╔╦╗╔═╗ ┌┐┌┌─┐┌┐┌ ┌─┐┌─┐┌─┐┬┌┐┌┌─┐┌┬┐┌─┐┌┬┐
  345. // ║ ╦║╣ ║║║║╣ ╠╦╝╠═╣ ║ ║╣ ││││ ││││───├─┘├─┤│ ┬││││├─┤ │ ├┤ ││
  346. // ╚═╝╚═╝╝╚╝╚═╝╩╚═╩ ╩ ╩ ╚═╝ ┘└┘└─┘┘└┘ ┴ ┴ ┴└─┘┴┘└┘┴ ┴ ┴ └─┘─┴┘
  347. // ┌┬┐┬ ┬┌─┐┌─┐ ┌┬┐┬ ┬┌─┐ ┬┌─┐┬┌┐┌ ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐
  348. // │ └┬┘├─┘├┤ │ ││││ │ ││ │││││ │ ├┤ │││├─┘│ ├─┤ │ ├┤
  349. // ┴ ┴ ┴ └─┘ ┴ └┴┘└─┘ └┘└─┘┴┘└┘ ┴ └─┘┴ ┴┴ ┴─┘┴ ┴ ┴ └─┘
  350. // If the join isn't using a join table and there isn't a `skip` or `limit`
  351. // criteria, a simple IN query can be built.
  352. if (strategy === 2 && !paginated) {
  353. (function generateTemplate() {
  354. // Ensure the criteria has a WHERE clause to make it valid
  355. if (!_.has(parentInstructions.criteria, 'where')) {
  356. parentInstructions.criteria.where = {};
  357. }
  358. // Convert the query to a statement
  359. try {
  360. (function buildUpConvertCriteria() {
  361. var convertObj = {
  362. model: parentInstructions.child + ' as ' + parentInstructions.childAlias,
  363. method: 'find',
  364. criteria: parentInstructions.criteria
  365. };
  366. if (schemaName) {
  367. convertObj.opts = {
  368. schema: schemaName
  369. };
  370. }
  371. statement = Converter(convertObj);
  372. })();
  373. } catch (e) {
  374. throw new Error('There was an error converting the Waterline Query into a Waterline Statement. ' + e.stack);
  375. }
  376. // Mixin the select from the top level instructions
  377. statement.select = parentInstructions.criteria.select;
  378. // Add in a WHERE IN template that can be rendered before compiling the
  379. // statement to include the primary keys of the parent.
  380. // This gives you a query like the following example:
  381. //
  382. // SELECT user.id from pet where pet.user_id IN [1,2,3,4];
  383. try {
  384. primaryKeyAttr = getPk(parentInstructions.child);
  385. } catch (e) {
  386. throw new Error('There was an issue getting the primary key attribute from ' + parentInstructions.child + ' are ' +
  387. 'you sure the getPk function is working correctly? It should accept a single argument which reperents the ' +
  388. 'tableName and should return a string of the column name that is set as the primary key of the table. \n\n' + e.stack);
  389. }
  390. // Build an IN template
  391. whereTemplate[parentInstructions.childAlias + '.' + parentInstructions.childKey] = {
  392. in: []
  393. };
  394. statement.where = statement.where || {};
  395. statement.where.and = statement.where.and || [];
  396. statement.where.and.push(whereTemplate);
  397. // Add the statement to the childStatements array
  398. childStatements.push({
  399. queryType: 'in',
  400. primaryKeyAttr: primaryKeyAttr,
  401. statement: statement,
  402. instructions: parentInstructions,
  403. alias: alias
  404. });
  405. })();
  406. }
  407. // ╔═╗╔═╗╔╗╔╔═╗╦═╗╔═╗╔╦╗╔═╗ ┌─┐┌─┐┌─┐┬┌┐┌┌─┐┌┬┐┌─┐┌┬┐
  408. // ║ ╦║╣ ║║║║╣ ╠╦╝╠═╣ ║ ║╣ ├─┘├─┤│ ┬││││├─┤ │ ├┤ ││
  409. // ╚═╝╚═╝╝╚╝╚═╝╩╚═╩ ╩ ╩ ╚═╝ ┴ ┴ ┴└─┘┴┘└┘┴ ┴ ┴ └─┘─┴┘
  410. // ┌┬┐┬ ┬┌─┐┌─┐ ┌┬┐┬ ┬┌─┐ ┬┌─┐┬┌┐┌ ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐
  411. // │ └┬┘├─┘├┤ │ ││││ │ ││ │││││ │ ├┤ │││├─┘│ ├─┤ │ ├┤
  412. // ┴ ┴ ┴ └─┘ ┴ └┴┘└─┘ └┘└─┘┴┘└┘ ┴ └─┘┴ ┴┴ ┴─┘┴ ┴ ┴ └─┘
  413. // If the join isn't using a join table but IS paginated then a big union query
  414. // will need to be generated. Generate a template for what a single piece of
  415. // the UNION ALL query will look like.
  416. if (strategy === 2 && paginated) {
  417. (function generateTemplate() {
  418. // Ensure the criteria has a WHERE clause to make it valid
  419. if (!_.has(parentInstructions.criteria, 'where')) {
  420. parentInstructions.criteria.where = {};
  421. }
  422. try {
  423. (function buildUpConvertCriteria() {
  424. var convertObj = {
  425. model: parentInstructions.child + ' as ' + parentInstructions.childAlias,
  426. method: 'find',
  427. criteria: parentInstructions.criteria
  428. };
  429. if (schemaName) {
  430. convertObj.opts = {
  431. schema: schemaName
  432. };
  433. }
  434. statement = Converter(convertObj);
  435. })();
  436. } catch (e) {
  437. throw new Error('There was an error converting the Waterline Query into a Waterline Statement.' + e.stack);
  438. }
  439. // Mixin the select from the top level instructions
  440. statement.select = parentInstructions.criteria.select;
  441. try {
  442. primaryKeyAttr = getPk(parentInstructions.child);
  443. } catch (e) {
  444. throw new Error('There was an issue getting the primary key attribute from ' + parentInstructions.child + ' are ' +
  445. 'you sure the getPk function is working correctly? It should accept a single argument which reperents the ' +
  446. 'tableName and should return a string of the column name that is set as the primary key of the table. \n\n' + e.stack);
  447. }
  448. // When using the UNION ALL type queries each query needs a where clause that
  449. // matches a single parent's primary key value. Use a ? for now and replace
  450. // it later with a real value.
  451. whereTemplate[parentInstructions.childAlias + '.' + parentInstructions.childKey] = '?';
  452. statement.where = statement.where || {};
  453. statement.where.and = statement.where.and || [];
  454. statement.where.and.push(whereTemplate);
  455. childStatements.push({
  456. queryType: 'union',
  457. primaryKeyAttr: primaryKeyAttr,
  458. statement: statement,
  459. instructions: parentInstructions,
  460. alias: alias
  461. });
  462. })();
  463. }
  464. // If the joins are using a join table then the statement template will need
  465. // the additional leftOuterJoin piece.
  466. // Grab the parent instructions
  467. var childInstructions = _.last(_instructions.instructions);
  468. // Check if the child is paginated
  469. var childPaginated = _.has(childInstructions.criteria, 'skip') || _.has(childInstructions.criteria, 'limit');
  470. // Ensure the criteria has a WHERE clause to make it valid
  471. if (!_.has(childInstructions.criteria, 'where')) {
  472. childInstructions.criteria.where = {};
  473. }
  474. // Ensure that child criteria are namespaced to the child alias, to avoid collisions
  475. // with fields in the join table (most likely `id`).
  476. childInstructions.criteria.where = (function disambiguate(obj) {
  477. return _.reduce(obj, function(memo, val, key) {
  478. if (key === 'and' || key === 'or') {
  479. memo[key] = _.map(val, disambiguate);
  480. }
  481. else {
  482. memo[childInstructions.childAlias + '.' + key] = val;
  483. }
  484. return memo;
  485. }, {});
  486. })(childInstructions.criteria.where);
  487. // ╔═╗╔═╗╔╗╔╔═╗╦═╗╔═╗╔╦╗╔═╗ ┌┐┌┌─┐┌┐┌ ┌─┐┌─┐┌─┐┬┌┐┌┌─┐┌┬┐┌─┐┌┬┐
  488. // ║ ╦║╣ ║║║║╣ ╠╦╝╠═╣ ║ ║╣ ││││ ││││───├─┘├─┤│ ┬││││├─┤ │ ├┤ ││
  489. // ╚═╝╚═╝╝╚╝╚═╝╩╚═╩ ╩ ╩ ╚═╝ ┘└┘└─┘┘└┘ ┴ ┴ ┴└─┘┴┘└┘┴ ┴ ┴ └─┘─┴┘
  490. // ┌┬┐┬ ┬┌─┐┌─┐ ┌┬┐┬ ┬┬─┐┌─┐┌─┐ ┬┌─┐┬┌┐┌ ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐
  491. // │ └┬┘├─┘├┤ │ ├─┤├┬┘├┤ ├┤ ││ │││││ │ ├┤ │││├─┘│ ├─┤ │ ├┤
  492. // ┴ ┴ ┴ └─┘ ┴ ┴ ┴┴└─└─┘└─┘ └┘└─┘┴┘└┘ ┴ └─┘┴ ┴┴ ┴─┘┴ ┴ ┴ └─┘
  493. // If the join criteria isn't paginated an IN query can be used.
  494. if (strategy === 3 && !childPaginated) {
  495. (function generateTemplate() {
  496. // The WHERE IN template for many to many queries is a little bit different.
  497. // Instead of using the primary key of the parent the parent key of the
  498. // join table is used.
  499. whereTemplate[parentInstructions.childAlias + '.' + parentInstructions.childKey] = {
  500. in: []
  501. };
  502. var modifiedInstructions = _.merge({}, _instructions);
  503. modifiedInstructions.instructions = [childInstructions];
  504. _.first(modifiedInstructions.instructions).forceAlias = true;
  505. // Convert the query to a statement
  506. try {
  507. (function buildUpConvertCriteria() {
  508. var convertObj = {
  509. model: parentInstructions.child + ' as ' + parentInstructions.childAlias,
  510. method: 'find',
  511. criteria: childInstructions.criteria,
  512. joins: [modifiedInstructions]
  513. };
  514. if (schemaName) {
  515. convertObj.opts = {
  516. schema: schemaName
  517. };
  518. }
  519. statement = Converter(convertObj);
  520. })();
  521. } catch (e) {
  522. throw new Error('There was an error converting the Waterline Query into a Waterline Statement. ' + e.stack);
  523. }
  524. // Mixin the select from the top level instructions and make sure the correct
  525. // table name is prepended to it.
  526. statement.select = _.map(childInstructions.criteria.select, function normalizeSelect(column) {
  527. return childInstructions.childAlias + '.' + column;
  528. });
  529. // Mixin the Where IN template logic
  530. statement.where = statement.where || {};
  531. statement.where.and = statement.where.and || [];
  532. statement.where.and.push(whereTemplate);
  533. // Add in the generated foriegn key select value so the records can be
  534. // nested together correctly.
  535. var selectStr = parentInstructions.childAlias + '.' + parentInstructions.childKey + ' as _parent_fk';
  536. statement.select.push(selectStr);
  537. // Add the statement to the childStatements array
  538. childStatements.push({
  539. queryType: 'in',
  540. statement: statement,
  541. instructions: [parentInstructions, childInstructions],
  542. alias: alias
  543. });
  544. })();
  545. }
  546. // ╔═╗╔═╗╔╗╔╔═╗╦═╗╔═╗╔╦╗╔═╗ ┌─┐┌─┐┌─┐┬┌┐┌┌─┐┌┬┐┌─┐┌┬┐
  547. // ║ ╦║╣ ║║║║╣ ╠╦╝╠═╣ ║ ║╣ ├─┘├─┤│ ┬││││├─┤ │ ├┤ ││
  548. // ╚═╝╚═╝╝╚╝╚═╝╩╚═╩ ╩ ╩ ╚═╝ ┴ ┴ ┴└─┘┴┘└┘┴ ┴ ┴ └─┘─┴┘
  549. // ┌┬┐┬ ┬┌─┐┌─┐ ┌┬┐┬ ┬┬─┐┌─┐┌─┐ ┬┌─┐┬┌┐┌ ┌┬┐┌─┐┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐
  550. // │ └┬┘├─┘├┤ │ ├─┤├┬┘├┤ ├┤ ││ │││││ │ ├┤ │││├─┘│ ├─┤ │ ├┤
  551. // ┴ ┴ ┴ └─┘ ┴ ┴ ┴┴└─└─┘└─┘ └┘└─┘┴┘└┘ ┴ └─┘┴ ┴┴ ┴─┘┴ ┴ ┴ └─┘
  552. // If the join criteria is paginated a very complex and slow UNION ALL query
  553. // must be built.
  554. if (strategy === 3 && childPaginated) {
  555. (function generateTemplate() {
  556. var modifiedInstructions = _.merge({}, _instructions);
  557. modifiedInstructions.instructions = [childInstructions];
  558. _.first(modifiedInstructions.instructions).forceAlias = true;
  559. try {
  560. (function buildUpConvertCriteria() {
  561. var convertObj = {
  562. model: parentInstructions.child + ' as ' + parentInstructions.childAlias,
  563. method: 'find',
  564. criteria: childInstructions.criteria,
  565. joins: [modifiedInstructions]
  566. };
  567. if (schemaName) {
  568. convertObj.opts = {
  569. schema: schemaName
  570. };
  571. }
  572. statement = Converter(convertObj);
  573. })();
  574. } catch (e) {
  575. throw new Error('There was an error converting the Waterline Query into a Waterline Statement.' + e.stack);
  576. }
  577. // When using the UNION ALL type queries each query needs a where clause that
  578. // matches a single parent's primary key value. Use a ? for now and replace
  579. // it later with a real value.
  580. whereTemplate[parentInstructions.childAlias + '.' + parentInstructions.childKey] = '?';
  581. // Mixin the select from the top level instructions and make sure the correct
  582. // table name is prepended to it.
  583. statement.select = _.map(childInstructions.criteria.select, function normalizeSelect(column) {
  584. return childInstructions.childAlias + '.' + column;
  585. });
  586. // Mixin the Where IN template logic
  587. statement.where = statement.where || {};
  588. statement.where.and = statement.where.and || [];
  589. statement.where.and.push(whereTemplate);
  590. // Add in the generated foriegn key select value so the records can be
  591. // nested together correctly.
  592. var selectStr = parentInstructions.childAlias + '.' + parentInstructions.childKey + ' as _parent_fk';
  593. statement.select.push(selectStr);
  594. childStatements.push({
  595. queryType: 'union',
  596. strategy: strategy,
  597. primaryKeyAttr: parentInstructions.childKey,
  598. statement: statement,
  599. instructions: instructions[alias].instructions,
  600. alias: alias
  601. });
  602. })();
  603. }
  604. });
  605. return {
  606. parentStatement: parentStatement,
  607. childStatements: childStatements,
  608. instructions: instructions
  609. };
  610. };