Merge pull request #1379 from gpazo/22634
[xtuple] / lib / orm / source / xt / javascript / data.sql
1 select xt.install_js('XT','Data','xtuple', $$
2
3 (function () {
4
5   /**
6    * @class
7    *
8    * The XT.Data class includes all functions necessary to process data source requests against the database.
9    * It should be instantiated as an object against which its funtion calls are made. This class enforces privilege
10    * control and as such is not and should not be dispatchable.
11    */
12
13   XT.Data = {
14
15     ARRAY_TYPE: 'A',
16     COMPOSITE_TYPE: 'C',
17     DATE_TYPE: 'D',
18     STRING_TYPE: 'S',
19
20     CREATED_STATE: 'create',
21     READ_STATE: 'read',
22     UPDATED_STATE: 'update',
23     DELETED_STATE: 'delete',
24
25     /**
26      * Build a SQL `where` clause based on privileges for name space and type,
27      * and conditions and parameters passed.
28      *
29      * @seealso fetch
30      *
31      * @param {String} Name space
32      * @param {String} Type
33      * @param {Array} Parameters - optional
34      * @returns {Object}
35      */
36     buildClauseOptimized: function (nameSpace, type, parameters, orderBy) {
37       parameters = parameters || [];
38
39       var that = this,
40         arrayIdentifiers = [],
41         arrayParams,
42         charSql,
43         childOrm,
44         clauses = [],
45         count = 1,
46         fromKeyProp,
47         groupByColumnParams = [],
48         identifiers = [],
49         joinIdentifiers = [],
50         orderByList = [],
51         orderByColumnList = [],
52         isArray = false,
53         op,
54         orClause,
55         orderByIdentifiers = [],
56         orderByColumnIdentifiers = [],
57         orderByParams = [],
58         orderByColumnParams = [],
59         joins = [],
60         orm = this.fetchOrm(nameSpace, type),
61         param,
62         params = [],
63         parts,
64         pcount,
65         pertinentExtension,
66         pgType,
67         prevOrm,
68         privileges = orm.privileges,
69         prop,
70         sourceTableAlias,
71         ret = {};
72
73       ret.conditions = "";
74       ret.parameters = [];
75
76       /* Handle privileges. */
77       if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
78       if (privileges &&
79           (!privileges.all ||
80             (privileges.all &&
81               (!this.checkPrivilege(privileges.all.read) &&
82               !this.checkPrivilege(privileges.all.update)))
83           ) &&
84           privileges.personal &&
85           (this.checkPrivilege(privileges.personal.read) ||
86             this.checkPrivilege(privileges.personal.update))
87         ) {
88
89         parameters.push({
90           attribute: privileges.personal.properties,
91           isLower: true,
92           isUsernamePrivFilter: true,
93           value: XT.username
94         });
95       }
96
97       /* Support the short cut wherein the client asks for a filter on a toOne with a
98         string. Technically they should use "theAttr.theAttrNaturalKey", but if they
99         don't, massage the inputs as if they did */
100       parameters.map(function (parameter) {
101         var attributeIsString = typeof parameter.attribute === 'string';
102           attributes = attributeIsString ? [parameter.attribute] : parameter.attribute;
103
104         attributes.map(function (attribute) {
105           var prop = XT.Orm.getProperty(orm, attribute),
106             propName = prop.name,
107             childOrm,
108             naturalKey,
109             index;
110
111           if ((prop.toOne || prop.toMany) && attribute.indexOf('.') < 0) {
112             /* Someone is querying on a toOne without using a path */
113             /* TODO: even if there's a path x.y, it's possible that it's still not
114               correct because the correct path maybe is x.y.naturalKeyOfY */
115             if (prop.toOne && prop.toOne.type) {
116               childOrm = that.fetchOrm(nameSpace, prop.toOne.type);
117             } else if (prop.toMany && prop.toMany.type) {
118               childOrm = that.fetchOrm(nameSpace, prop.toMany.type);
119             } else {
120               plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
121             }
122             naturalKey = XT.Orm.naturalKey(childOrm);
123             if (attributeIsString) {
124               /* add the natural key to the end of the requested attribute */
125               parameter.attribute = attribute + "." + naturalKey;
126             } else {
127               /* swap out the attribute in the array for the one with the prepended natural key */
128               index = parameter.attribute.indexOf(attribute);
129               parameter.attribute.splice(index, 1);
130               parameter.attribute.push(attribute + "." + naturalKey);
131             }
132           }
133         });
134       });
135
136       /* Handle parameters. */
137       if (parameters.length) {
138         for (var i = 0; i < parameters.length; i++) {
139           orClause = [];
140           param = parameters[i];
141           op = param.operator || '=';
142           switch (op) {
143           case '=':
144           case '>':
145           case '<':
146           case '>=':
147           case '<=':
148           case '!=':
149             break;
150           case 'BEGINS_WITH':
151             op = '~^';
152             break;
153           case 'ENDS_WITH':
154             op = '~?';
155             break;
156           case 'MATCHES':
157             op = '~*';
158             break;
159           case 'ANY':
160             op = '<@';
161             for (var c = 0; c < param.value.length; c++) {
162               ret.parameters.push(param.value[c]);
163               param.value[c] = '$' + count;
164               count++;
165             }
166             break;
167           case 'NOT ANY':
168             op = '!<@';
169             for (var c = 0; c < param.value.length; c++) {
170               ret.parameters.push(param.value[c]);
171               param.value[c] = '$' + count;
172               count++;
173             }
174             break;
175           default:
176             plv8.elog(ERROR, 'Invalid operator: ' + op);
177           }
178
179           /* Handle characteristics. This is very specific to xTuple,
180              and highly dependant on certain table structures and naming conventions,
181              but otherwise way too much work to refactor in an abstract manner right now. */
182           if (param.isCharacteristic) {
183             /* Handle array. */
184             if (op === '<@') {
185               param.value = ' ARRAY[' + param.value.join(',') + ']';
186             }
187
188             /* Booleans are stored as strings. */
189             if (param.value === true) {
190               param.value = 't';
191             } else if (param.value === false) {
192               param.value = 'f';
193             }
194
195             /* Yeah, it depends on a property called 'characteristics'... */
196             prop = XT.Orm.getProperty(orm, 'characteristics');
197
198             /* Build the characteristics query clause. */
199             identifiers.push(XT.Orm.primaryKey(orm, true));
200             identifiers.push(prop.toMany.inverse);
201             identifiers.push(orm.nameSpace.toLowerCase());
202             identifiers.push(prop.toMany.type.decamelize());
203             identifiers.push(param.attribute);
204             identifiers.push(param.value);
205
206             charSql = '%' + (identifiers.length - 5) + '$I in (' +
207                       '  select %' + (identifiers.length - 4) + '$I '+
208                       '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
209                       '    join char on (char_name = characteristic)' +
210                       '  where 1=1 ' +
211                       /* Note: Not using $i for these. L = literal here. These is not identifiers. */
212                       '    and char_name = %' + (identifiers.length - 1) + '$L ' +
213                       '    and value ' + op + ' %' + (identifiers.length) + '$L ' +
214                       ')';
215
216             clauses.push(charSql);
217
218           /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
219           } else if (op === '<@' || op === '!<@') {
220             /* Handle paths if applicable. */
221             if (param.attribute.indexOf('.') > -1) {
222               parts = param.attribute.split('.');
223               childOrm = this.fetchOrm(nameSpace, type);
224               params.push("");
225               pcount = params.length - 1;
226
227               for (var n = 0; n < parts.length; n++) {
228                 /* Validate attribute. */
229                 prop = XT.Orm.getProperty(childOrm, parts[n]);
230                 if (!prop) {
231                   plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
232                 }
233
234                 /* Build path. */
235                 if (n === parts.length - 1) {
236                   identifiers.push("jt" + (joins.length - 1));
237                   identifiers.push(prop.attr.column);
238                   pgType = this.getPgTypeFromOrmType(
239                     this.getNamespaceFromNamespacedTable(childOrm.table),
240                     this.getTableFromNamespacedTable(childOrm.table),
241                     prop.attr.column
242                   );
243                   pgType = pgType ? "::" + pgType + "[]" : '';
244                   params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
245                   params[pcount] += ' ' + op + ' ARRAY[' + param.value.join(',') + ']' + pgType;
246                 } else {
247                   childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
248                   sourceTableAlias = n === 0 ? "t1" : "jt" + (joins.length - 1);
249                   joinIdentifiers.push(
250                     this.getNamespaceFromNamespacedTable(childOrm.table),
251                     this.getTableFromNamespacedTable(childOrm.table),
252                     sourceTableAlias, prop.toOne.column,
253                     XT.Orm.primaryKey(childOrm, true));
254                   joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
255                     + "$I jt" + joins.length + " on %"
256                     + (joinIdentifiers.length - 2) + "$I.%"
257                     + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
258                 }
259               }
260             } else {
261               prop = XT.Orm.getProperty(orm, param.attribute);
262               pertinentExtension = XT.Orm.getProperty(orm, param.attribute, true);
263               if(pertinentExtension.isChild || pertinentExtension.isExtension) {
264                 /* We'll need to join this orm extension */
265                 fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
266                 joinIdentifiers.push(
267                   this.getNamespaceFromNamespacedTable(pertinentExtension.table),
268                   this.getTableFromNamespacedTable(pertinentExtension.table),
269                   fromKeyProp.attr.column,
270                   pertinentExtension.relations[0].column);
271                 joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
272                   + "$I jt" + joins.length + " on t1.%"
273                   + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
274               }
275               if (!prop) {
276                 plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute);
277               }
278
279               identifiers.push(pertinentExtension.isChild || pertinentExtension.isExtension ? 
280                 "jt" + (joins.length - 1) : 
281                 "t1");
282               identifiers.push(prop.attr.column);
283               pgType = this.getPgTypeFromOrmType(
284                 this.getNamespaceFromNamespacedTable(orm.table),
285                 this.getTableFromNamespacedTable(orm.table),
286                 prop.attr.column
287               );
288               pgType = pgType ? "::" + pgType + "[]" : '';
289               params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']' + pgType);
290               pcount = params.length - 1;
291             }
292             clauses.push(params[pcount]);
293
294           /* Everything else handle another. */
295           } else {
296             if (XT.typeOf(param.attribute) !== 'array') {
297               param.attribute = [param.attribute];
298             }
299
300             for (var c = 0; c < param.attribute.length; c++) {
301               /* Handle paths if applicable. */
302               if (param.attribute[c].indexOf('.') > -1) {
303                 parts = param.attribute[c].split('.');
304                 childOrm = this.fetchOrm(nameSpace, type);
305                 params.push("");
306                 pcount = params.length - 1;
307                 isArray = false;
308
309                 /* Check if last part is an Array. */
310                 for (var m = 0; m < parts.length; m++) {
311                   /* Validate attribute. */
312                   prop = XT.Orm.getProperty(childOrm, parts[m]);
313                   if (!prop) {
314                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
315                   }
316
317                   if (m < parts.length - 1) {
318                     if (prop.toOne && prop.toOne.type) {
319                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
320                     } else if (prop.toMany && prop.toMany.type) {
321                       childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
322                     } else {
323                       plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
324                     }
325                   } else if (prop.attr && prop.attr.type === 'Array') {
326                     /* The last property in the path is an array. */
327                     isArray = true;
328                     params[pcount] = '$' + count;
329                   }
330                 }
331
332                 /* Reset the childOrm to parent. */
333                 childOrm = this.fetchOrm(nameSpace, type);
334
335                 for (var n = 0; n < parts.length; n++) {
336                   /* Validate attribute. */
337                   prop = XT.Orm.getProperty(childOrm, parts[n]);
338                   if (!prop) {
339                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
340                   }
341
342                   /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
343                   if (param.isUsernamePrivFilter && isArray) {
344                     identifiers.push(prop.attr.column);
345                     arrayIdentifiers.push(identifiers.length);
346
347                     if (n < parts.length - 1) {
348                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
349                     }
350                   } else {
351                     /* Build path, e.g. table_name.column_name */
352                     if (n === parts.length - 1) {
353                       identifiers.push("jt" + (joins.length - 1));
354                       identifiers.push(prop.attr.column);
355                       params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
356                       if (param.isLower) {
357                         params[pcount] = "lower(" + params[pcount] + ")";
358                       }
359                     } else {
360                       sourceTableAlias = n === 0 ? "t1" : "jt" + (joins.length - 1);
361                       if (prop.toOne && prop.toOne.type) {
362                         childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
363                         joinIdentifiers.push(
364                           this.getNamespaceFromNamespacedTable(childOrm.table),
365                           this.getTableFromNamespacedTable(childOrm.table),
366                           sourceTableAlias, prop.toOne.column,
367                           XT.Orm.primaryKey(childOrm, true)
368                         );
369                       } else if (prop.toMany && prop.toMany.type) {
370                         childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
371                         joinIdentifiers.push(
372                           this.getNamespaceFromNamespacedTable(childOrm.table),
373                           this.getTableFromNamespacedTable(childOrm.table),
374                           sourceTableAlias, prop.toMany.column,
375                           XT.Orm.primaryKey(childOrm, true)
376                         );
377                       }
378                       joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
379                         + "$I jt" + joins.length + " on %"
380                         + (joinIdentifiers.length - 2) + "$I.%"
381                         + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
382                     }
383                   }
384                 }
385               } else {
386                 /* Validate attribute. */
387                 prop = XT.Orm.getProperty(orm, param.attribute[c]);
388                 pertinentExtension = XT.Orm.getProperty(orm, param.attribute[c], true);
389                 if(pertinentExtension.isChild || pertinentExtension.isExtension) {
390                   /* We'll need to join this orm extension */
391                   fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
392                   joinIdentifiers.push(
393                     this.getNamespaceFromNamespacedTable(pertinentExtension.table),
394                     this.getTableFromNamespacedTable(pertinentExtension.table),
395                     fromKeyProp.attr.column,
396                     pertinentExtension.relations[0].column);
397                   joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
398                     + "$I jt" + joins.length + " on t1.%"
399                     + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
400                 }
401                 if (!prop) {
402                   plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
403                 }
404
405                 identifiers.push(pertinentExtension.isChild || pertinentExtension.isExtension ? 
406                   "jt" + (joins.length - 1) : 
407                   "t1");
408                 identifiers.push(prop.attr.column);
409
410                 /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
411                 if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
412                   (prop.attr && prop.attr.type === 'Array'))) {
413
414                   params.push('$' + count);
415                   pcount = params.length - 1;
416                   arrayIdentifiers.push(identifiers.length);
417                 } else {
418                   params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I");
419                   pcount = params.length - 1;
420                 }
421               }
422
423               /* Add persional privs array search. */
424               if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
425                 || (prop.attr && prop.attr.type === 'Array') || isArray)) {
426
427                 /* XXX: this bit of code has not been touched by the optimization refactor */
428                 /* e.g. 'admin' = ANY (usernames_array) */
429                 arrayParams = "";
430                 params[pcount] += ' ' + op + ' ANY (';
431
432                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
433                 for (var f =0; f < arrayIdentifiers.length; f++) {
434                   arrayParams += '%' + arrayIdentifiers[f] + '$I';
435                   if (f < arrayIdentifiers.length - 1) {
436                     arrayParams = "(" + arrayParams + ").";
437                   }
438                 }
439                 params[pcount] += arrayParams + ')';
440
441               /* Add optional is null clause. */
442               } else if (parameters[i].includeNull) {
443                 /* e.g. %1$I = $1 or %1$I is null */
444                 params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
445               } else {
446                 /* e.g. %1$I = $1 */
447                 params[pcount] += " " + op + ' $' + count;
448               }
449
450               orClause.push(params[pcount]);
451             }
452
453             /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
454             clauses.push('(' + orClause.join(' or ') + ')');
455             count++;
456             ret.parameters.push(param.value);
457           }
458         }
459       }
460
461       ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
462
463       /* Massage orderBy with quoted identifiers. */
464       /* We need to support the xm case for sql2 and the xt/public (column) optimized case for sql1 */
465       /* In practice we build the two lists independently of one another */
466       if (orderBy) {
467         for (var i = 0; i < orderBy.length; i++) {
468           /* Handle path case. */
469           if (orderBy[i].attribute.indexOf('.') > -1) {
470             parts = orderBy[i].attribute.split('.');
471             prevOrm = orm;
472             orderByParams.push("");
473             orderByColumnParams.push("");
474             groupByColumnParams.push("");
475             pcount = orderByParams.length - 1;
476
477             for (var n = 0; n < parts.length; n++) {
478               prop = XT.Orm.getProperty(orm, parts[n]);
479               if (!prop) {
480                 plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
481               }
482               orderByIdentifiers.push(parts[n]);
483               orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
484
485               if (n === parts.length - 1) {
486                 orderByColumnIdentifiers.push("jt" + (joins.length - 1));
487                 orderByColumnIdentifiers.push(prop.attr.column);
488                 orderByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
489                 groupByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
490               } else {
491                 orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
492                 orm = this.fetchOrm(nameSpace, prop.toOne.type);
493                 sourceTableAlias = n === 0 ? "t1" : "jt" + (joins.length - 1);
494                 joinIdentifiers.push(
495                   this.getNamespaceFromNamespacedTable(orm.table),
496                   this.getTableFromNamespacedTable(orm.table),
497                   sourceTableAlias, prop.toOne.column,
498                   XT.Orm.primaryKey(orm, true));
499                 joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
500                   + "$I jt" + joins.length + " on %"
501                   + (joinIdentifiers.length - 2) + "$I.%"
502                   + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
503               }
504             }
505             orm = prevOrm;
506           /* Normal case. */
507           } else {
508             prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
509             if (!prop) {
510               plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
511             }
512             orderByIdentifiers.push(orderBy[i].attribute);
513             orderByColumnIdentifiers.push("t1");
514             /*
515               We might need to look at toOne if the client is asking for a toOne without specifying
516               the path. Unfortunately, if they do specify the path, then sql2 will fail. So this does
517               work, although we're really sorting by the primary key of the toOne, whereas the 
518               user probably wants us to sort by the natural key TODO
519             */
520             orderByColumnIdentifiers.push(prop.attr ? prop.attr.column : prop.toOne.column);
521             orderByParams.push("%" + orderByIdentifiers.length + "$I");
522             orderByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
523             groupByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
524             pcount = orderByParams.length - 1;
525           }
526
527           if (orderBy[i].isEmpty) {
528             orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
529             orderByColumnParams[pcount] = "length(" + orderByColumnParams[pcount] + ")=0";
530           }
531           if (orderBy[i].descending) {
532             orderByParams[pcount] += " desc";
533             orderByColumnParams[pcount] += " desc";
534           }
535
536           orderByList.push(orderByParams[pcount])
537           orderByColumnList.push(orderByColumnParams[pcount])
538         }
539       }
540
541       ret.orderBy = orderByList.length ? XT.format('order by ' + orderByList.join(','), orderByIdentifiers) : '';
542       ret.orderByColumns = orderByColumnList.length ? XT.format('order by ' + orderByColumnList.join(','), orderByColumnIdentifiers) : '';
543       ret.groupByColumns = groupByColumnParams.length ? XT.format(', ' + groupByColumnParams.join(','), orderByColumnIdentifiers) : '';
544       ret.joins = joins.length ? XT.format(joins.join(' '), joinIdentifiers) : '';
545
546       return ret;
547     },
548
549     /**
550      * Queries whether the current user has been granted the privilege passed.
551      *
552      * @param {String} privilege
553      * @returns {Boolean}
554      */
555     checkPrivilege: function (privilege) {
556       var i,
557         privArray,
558         res,
559         ret = privilege,
560         sql;
561
562       if (typeof privilege === 'string') {
563         if (!this._granted) { this._granted = {}; }
564         if (!this._granted[XT.username]) { this._granted[XT.username] = {}; }
565         if (this._granted[XT.username][privilege] !== undefined) { return this._granted[XT.username][privilege]; }
566
567         /* The privilege name is allowed to be a set of space-delimited privileges */
568         /* If a user has any of the applicable privileges then they get access */
569         privArray = privilege.split(" ");
570         sql = 'select coalesce(usrpriv_priv_id, grppriv_priv_id, -1) > 0 as granted ' +
571                'from priv ' +
572                'left join usrpriv on (priv_id=usrpriv_priv_id) and (usrpriv_username=$1) ' +
573                'left join ( ' +
574                '  select distinct grppriv_priv_id ' +
575                '  from grppriv ' +
576                '    join usrgrp on (grppriv_grp_id=usrgrp_grp_id) and (usrgrp_username=$1) ' +
577                '  ) grppriv on (grppriv_priv_id=priv_id) ' +
578                'where priv_name = $2';
579
580         for (var i = 1; i < privArray.length; i++) {
581           sql = sql + ' or priv_name = $' + (i + 2);
582         }
583         sql = sql + "order by granted desc limit 1;";
584
585         /* Cleverness: the query parameters are just the priv array with the username tacked on front. */
586         privArray.unshift(XT.username);
587
588         if (DEBUG) {
589           XT.debug('checkPrivilege sql =', sql);
590           XT.debug('checkPrivilege values =', privArray);
591         }
592         res = plv8.execute(sql, privArray);
593         ret = res.length ? res[0].granted : false;
594
595         /* Memoize. */
596         this._granted[XT.username][privilege] = ret;
597       }
598
599       if (DEBUG) {
600         XT.debug('Privilege check for "' + XT.username + '" on "' + privilege + '" returns ' + ret);
601       }
602
603       return ret;
604     },
605
606     /**
607      * Validate whether user has read access to data. If a record is passed, check personal privileges of
608      * that record.
609      *
610      * @param {String} name space
611      * @param {String} type name
612      * @param {Object} record - optional
613      * @param {Boolean} is top level, default is true
614      * @returns {Boolean}
615      */
616     checkPrivileges: function (nameSpace, type, record, isTopLevel) {
617       isTopLevel = isTopLevel !== false ? true : false;
618       var action =  record && record.dataState === this.CREATED_STATE ? 'create' :
619                   record && record.dataState === this.DELETED_STATE ? 'delete' :
620                   record && record.dataState === this.UPDATED_STATE ? 'update' : 'read',
621         committing = record ? record.dataState !== this.READ_STATE : false,
622         isGrantedAll = true,
623         isGrantedPersonal = false,
624         map = this.fetchOrm(nameSpace, type),
625         privileges = map.privileges,
626         pkey,
627         old;
628
629       /* If there is no ORM, this isn't a table data type so no check required. */
630       /*
631       if (DEBUG) {
632         XT.debug('orm type is ->', map.type);
633         XT.debug('orm is ->', map);
634       }
635       */
636       if (!map) { return true; }
637
638       /* Can not access 'nested only' records directly. */
639       if (DEBUG) {
640         XT.debug('is top level ->', isTopLevel);
641         XT.debug('is nested ->', map.isNestedOnly);
642       }
643       if (isTopLevel && map.isNestedOnly) { return false; }
644
645       /* Check privileges - first do we have access to anything? */
646       if (privileges) {
647         if (DEBUG) { XT.debug('privileges found', privileges); }
648         if (committing) {
649           if (DEBUG) { XT.debug('is committing'); }
650
651           /* Check if user has 'all' read privileges. */
652           isGrantedAll = privileges.all ? this.checkPrivilege(privileges.all[action]) : false;
653
654           /* Otherwise check for 'personal' read privileges. */
655           if (!isGrantedAll) {
656             isGrantedPersonal =  privileges.personal ?
657               this.checkPrivilege(privileges.personal[action]) : false;
658           }
659         } else {
660           if (DEBUG) { XT.debug('is NOT committing'); }
661
662           /* Check if user has 'all' read privileges. */
663           isGrantedAll = privileges.all ?
664                          this.checkPrivilege(privileges.all.read) ||
665                          this.checkPrivilege(privileges.all.update) : false;
666
667           /* Otherwise check for 'personal' read privileges. */
668           if (!isGrantedAll) {
669             isGrantedPersonal =  privileges.personal ?
670               this.checkPrivilege(privileges.personal.read) ||
671               this.checkPrivilege(privileges.personal.update) : false;
672           }
673         }
674       }
675
676       /* If we're checknig an actual record and only have personal privileges, */
677       /* see if the record allows access. */
678       if (record && !isGrantedAll && isGrantedPersonal && action !== "create") {
679         if (DEBUG) { XT.debug('checking record level personal privileges'); }
680         var that = this,
681
682         /* Shared checker function that checks 'personal' properties for access rights. */
683         checkPersonal = function (record) {
684           var i = 0,
685             isGranted = false,
686             props = privileges.personal.properties,
687             get = function (obj, target) {
688               var idx,
689                 part,
690                 parts = target.split("."),
691                 ret;
692
693               for (var idx = 0; idx < parts.length; idx++) {
694                 part = parts[idx];
695                 ret = ret ? ret[part] : obj[part];
696                 if (ret === null || ret === undefined) {
697                   return null;
698                 }
699               }
700
701               return ret;
702             };
703
704           while (!isGranted && i < props.length) {
705             var prop = props[i],
706                 personalUser = get(record, prop);
707
708             if (personalUser instanceof Array) {
709               for (var userIdx = 0; userIdx < personalUser.length; userIdx++) {
710                 if (personalUser[userIdx].toLowerCase() === XT.username) {
711                   isGranted = true;
712                 }
713               }
714             } else if (personalUser) {
715               isGranted = personalUser.toLowerCase() === XT.username;
716             }
717
718             i++;
719           }
720
721           return isGranted;
722         };
723
724         /* If committing we need to ensure the record in its previous state is editable by this user. */
725         if (committing && (action === 'update' || action === 'delete')) {
726           pkey = XT.Orm.naturalKey(map) || XT.Orm.primaryKey(map);
727           old = this.retrieveRecord({
728             nameSpace: nameSpace,
729             type: type,
730             id: record[pkey],
731             superUser: true,
732             includeKeys: true
733           });
734           isGrantedPersonal = checkPersonal(old.data);
735
736         /* Otherwise check personal privileges on the record passed. */
737         } else if (action === 'read') {
738           isGrantedPersonal = checkPersonal(record);
739         }
740       }
741
742       if (DEBUG) {
743         XT.debug('is granted all ->', isGrantedAll);
744         XT.debug('is granted personal ->', isGrantedPersonal);
745       }
746
747       return isGrantedAll || isGrantedPersonal;
748     },
749
750     /**
751      * Commit array columns with their own statements
752      *
753      * @param {Object} Orm
754      * @param {Object} Record
755      */
756     commitArrays: function (orm, record, encryptionKey) {
757       var pkey = XT.Orm.primaryKey(orm),
758         fkey,
759         ormp,
760         prop,
761         val,
762         values,
763         columnToKey,
764         propToKey,
765
766         resolveKey = function (col) {
767           var attr;
768
769           /* First search properties */
770           var ary = orm.properties.filter(function (prop) {
771             return prop.attr && prop.attr.column === col;
772           });
773
774           if (ary.length) {
775             attr =  ary[0].name;
776
777           } else {
778             /* If not found must be extension, search relations */
779             if (orm.extensions.length) {
780               orm.extensions.forEach(function (ext) {
781                 if (!attr) {
782                   ary = ext.relations.filter(function (prop) {
783                     return prop.column === col;
784                   });
785
786                   if (ary.length) {
787                     attr = ary[0].inverse;
788                   }
789                 }
790               })
791             };
792           }
793           if (attr) { return attr };
794
795           /* If still not found, we have a structural problem */
796           throw new Error("Can not resolve primary id on toMany relation");
797         };
798
799       for (prop in record) {
800         ormp = XT.Orm.getProperty(orm, prop);
801
802         /* If the property is an array of objects they must be records so commit them. */
803         if (ormp.toMany && ormp.toMany.isNested) {
804           fkey = ormp.toMany.inverse;
805           values = record[prop];
806
807           for (var i = 0; i < values.length; i++) {
808             val = values[i];
809
810             /* Populate the parent key into the foreign key field if it's absent. */
811             if (!val[fkey]) {
812               columnToKey = ormp.toMany.column;
813               propToKey = columnToKey ? resolveKey(columnToKey) : pkey;
814               if (!record[propToKey]) {
815                 /* If there's no data, we have a structural problem */
816                 throw new Error("Can not resolve foreign key on toMany relation " + ormp.name);
817               }
818               val[fkey] = record[propToKey];
819             }
820
821             this.commitRecord({
822               nameSpace: orm.nameSpace,
823               type: ormp.toMany.type,
824               data: val,
825               encryptionKey: encryptionKey
826             });
827           }
828         }
829       }
830     },
831
832     /**
833      * Commit metrics that have changed to the database.
834      *
835      * @param {Object} metrics
836      * @returns Boolean
837      */
838     commitMetrics: function (metrics) {
839       var key,
840         sql = 'select setMetric($1,$2)',
841         value;
842
843       for (key in metrics) {
844         value = metrics[key];
845         if (typeof value === 'boolean') {
846           value = value ? 't' : 'f';
847         } else if (typeof value === 'number') {
848           value = value.toString();
849         }
850
851         if (DEBUG) {
852           XT.debug('commitMetrics sql =', sql);
853           XT.debug('commitMetrics values =', [key, value]);
854         }
855         plv8.execute(sql, [key, value]);
856       }
857
858       return true;
859     },
860
861     /**
862      * Commit a record to the database. The record must conform to the object hiearchy as defined by the
863      * record's `ORM` definition. Each object in the tree must include state information on a reserved property
864      * called `dataState`. Valid values are `create`, `update` and `delete`. Objects with other dataState values including
865      * `undefined` will be ignored. State values can be added using `XT.jsonpatch.updateState(obj, state)`.
866      *
867      * @seealso XT.jsonpatch.updateState
868      * @param {Object} Options
869      * @param {String} [options.nameSpace] Namespace. Required.
870      * @param {String} [options.type] Type. Required.
871      * @param {Object} [options.data] The data payload to be processed. Required
872      * @param {Number} [options.etag] Record version for optimistic locking.
873      * @param {Object} [options.lock] Lock information for pessemistic locking.
874      * @param {Boolean} [options.superUser=false] If true ignore privilege checking.
875      * @param {String} [options.encryptionKey] Encryption key.
876      */
877     commitRecord: function (options) {
878       var data = options.data,
879         dataState = data ? data.dataState : false,
880         hasAccess = options.superUser ||
881           this.checkPrivileges(options.nameSpace, options.type, data, false);
882
883       if (!hasAccess) { throw new Error("Access Denied."); }
884       switch (dataState)
885       {
886       case (this.CREATED_STATE):
887         this.createRecord(options);
888         break;
889       case (this.UPDATED_STATE):
890         this.updateRecord(options);
891         break;
892       case (this.DELETED_STATE):
893         this.deleteRecord(options);
894       }
895     },
896
897     /**
898      * Commit insert to the database
899      *
900      * @param {Object} Options
901      * @param {String} [options.nameSpace] Namespace. Required.
902      * @param {String} [options.type] Type. Required.
903      * @param {Object} [options.data] The data payload to be processed. Required.
904      * @param {String} [options.encryptionKey] Encryption key.
905      */
906     createRecord: function (options) {
907       var data = options.data,
908         encryptionKey = options.encryptionKey,
909         i,
910         orm = this.fetchOrm(options.nameSpace, options.type),
911         sql = this.prepareInsert(orm, data, null, encryptionKey),
912         pkey = XT.Orm.primaryKey(orm),
913         rec;
914
915       /* Handle extensions on the same table. */
916       for (var i = 0; i < orm.extensions.length; i++) {
917         if (orm.extensions[i].table === orm.table) {
918           sql = this.prepareInsert(orm.extensions[i], data, sql, encryptionKey);
919         }
920       }
921
922       /* Commit the base record. */
923       if (DEBUG) {
924         XT.debug('createRecord sql =', sql.statement);
925         XT.debug('createRecord values =', sql.values);
926       }
927
928       if (sql.statement) {
929         rec = plv8.execute(sql.statement, sql.values);
930         /* Make sure the primary key is populated */
931         if (!data[pkey]) {
932           data[pkey] = rec[0].id;
933         }
934         /* Make sure the obj_uuid is populated, if applicable */
935         if (!data.obj_uuid && rec[0] && rec[0].obj_uuid) {
936           data.uuid = rec[0].obj_uuid;
937         }
938       }
939
940       /* Handle extensions on other tables. */
941       for (var i = 0; i < orm.extensions.length; i++) {
942         if (orm.extensions[i].table !== orm.table &&
943            !orm.extensions[i].isChild) {
944           sql = this.prepareInsert(orm.extensions[i], data, null, encryptionKey);
945
946           if (DEBUG) {
947             XT.debug('createRecord sql =', sql.statement);
948             XT.debug('createRecord values =', sql.values);
949           }
950
951           if (sql.statement) {
952             plv8.execute(sql.statement, sql.values);
953           }
954         }
955       }
956
957       /* Okay, now lets handle arrays. */
958       this.commitArrays(orm, data, encryptionKey);
959     },
960
961     /**
962      * Use an orm object and a record and build an insert statement. It
963      * returns an object with a table name string, columns array, expressions
964      * array and insert statement string that can be executed.
965      *
966      * The optional params object includes objects columns, expressions
967      * that can be cumulatively added to the result.
968      *
969      * @params {Object} Orm
970      * @params {Object} Record
971      * @params {Object} Params - optional
972      * @params {String} Encryption Key
973      * @returns {Object}
974      */
975     prepareInsert: function (orm, record, params, encryptionKey) {
976       var attr,
977         attributePrivileges,
978         columns,
979         count,
980         encryptQuery,
981         encryptSql,
982         exp,
983         i,
984         iorm,
985         namespace,
986         nkey,
987         ormp,
988         pkey = XT.Orm.primaryKey(orm),
989         prop,
990         query,
991         sql = "select nextval($1) as id",
992         table,
993         toOneQuery,
994         toOneSql,
995         type,
996         val,
997         isValidSql = params && params.statement ? true : false,
998         canEdit;
999
1000       params = params || {
1001         table: "",
1002         columns: [],
1003         expressions: [],
1004         identifiers: [],
1005         values: []
1006       };
1007       params.table = orm.table;
1008       count = params.values.length + 1;
1009
1010       /* If no primary key, then create one. */
1011       if (!record[pkey] && orm.idSequenceName) {
1012         if (DEBUG) {
1013           XT.debug('prepareInsert sql =', sql);
1014           XT.debug('prepareInsert values =', [orm.idSequenceName]);
1015         }
1016         record[pkey] = plv8.execute(sql, [orm.idSequenceName])[0].id;
1017       }
1018
1019       /* If extension handle key. */
1020       if (orm.relations) {
1021         for (var i = 0; i < orm.relations.length; i++) {
1022           column = orm.relations[i].column;
1023           if (!params.identifiers.contains(column)) {
1024             params.columns.push("%" + count + "$I");
1025             params.values.push(record[orm.relations[i].inverse]);
1026             params.expressions.push('$' + count);
1027             params.identifiers.push(orm.relations[i].column);
1028             count++;
1029           }
1030         }
1031       }
1032
1033       /* Build up the content for insert of this record. */
1034       for (var i = 0; i < orm.properties.length; i++) {
1035         ormp = orm.properties[i];
1036         prop = ormp.name;
1037
1038         if (ormp.toMany && ormp.toMany.column === 'obj_uuid') {
1039           params.parentUuid = true;
1040         }
1041
1042         attr = ormp.attr ? ormp.attr : ormp.toOne ? ormp.toOne : ormp.toMany;
1043         type = attr.type;
1044         iorm = ormp.toOne ? this.fetchOrm(orm.nameSpace, ormp.toOne.type) : false,
1045         nkey = iorm ? XT.Orm.naturalKey(iorm, true) : false;
1046         val = ormp.toOne && record[prop] instanceof Object ?
1047           record[prop][nkey || ormp.toOne.inverse || 'id'] : record[prop];
1048
1049         /**
1050          * Ignore derived fields for insert/update
1051          */
1052         if (attr.derived) continue;
1053
1054         attributePrivileges = orm.privileges &&
1055           orm.privileges.attribute &&
1056           orm.privileges.attribute[prop];
1057
1058         if(!attributePrivileges || attributePrivileges.create === undefined) {
1059           canEdit = true;
1060         } else if (typeof attributePrivileges.create === 'string') {
1061           canEdit = this.checkPrivilege(attributePrivileges.create);
1062         } else {
1063           canEdit = attributePrivileges.create; /* if it's true or false */
1064         }
1065
1066         /* Handle fixed values. */
1067         if (attr.value !== undefined) {
1068           params.columns.push("%" + count + "$I");
1069           params.expressions.push('$' + count);
1070           params.values.push(attr.value);
1071           params.identifiers.push(attr.column);
1072           isValidSql = true;
1073           count++;
1074
1075         /* Handle passed values. */
1076         } else if (canEdit && val !== undefined && val !== null && !ormp.toMany) {
1077           if (attr.isEncrypted) {
1078             if (encryptionKey) {
1079               encryptQuery = "select encrypt(setbytea(%1$L), setbytea(%2$L), %3$L)";
1080               encryptSql = XT.format(encryptQuery, [record[prop], encryptionKey, 'bf']);
1081               val = record[prop] ? plv8.execute(encryptSql)[0].encrypt : null;
1082               params.columns.push("%" + count + "$I");
1083               params.values.push(val);
1084               params.identifiers.push(attr.column);
1085               params.expressions.push("$" + count);
1086               isValidSql = true;
1087               count++;
1088             } else {
1089               throw new Error("No encryption key provided.");
1090             }
1091           } else {
1092             if (ormp.toOne && nkey) {
1093               if (iorm.table.indexOf(".") > 0) {
1094                 toOneQuery = "select %1$I from %2$I.%3$I where %4$I = $" + count;
1095                 toOneSql = XT.format(toOneQuery, [
1096                     XT.Orm.primaryKey(iorm, true),
1097                     iorm.table.beforeDot(),
1098                     iorm.table.afterDot(),
1099                     nkey
1100                   ]);
1101               } else {
1102                 toOneQuery = "select %1$I from %2$I where %3$I = $" + count;
1103                 toOneSql = XT.format(toOneQuery, [
1104                     XT.Orm.primaryKey(iorm, true),
1105                     iorm.table,
1106                     nkey
1107                   ]);
1108               }
1109               exp = "(" + toOneSql + ")";
1110               params.expressions.push(exp);
1111             } else {
1112               params.expressions.push('$' + count);
1113             }
1114
1115             params.columns.push("%" + count + "$I");
1116             params.values.push(val);
1117             params.identifiers.push(attr.column);
1118             isValidSql = true;
1119             count++;
1120           }
1121         /* Handle null value if applicable. */
1122         } else if (canEdit && val === undefined || val === null) {
1123           if (attr.nullValue) {
1124             params.columns.push("%" + count + "$I");
1125             params.values.push(attr.nullValue);
1126             params.identifiers.push(attr.column);
1127             params.expressions.push('$' + count);
1128             isValidSql = true;
1129             count++;
1130           } else if (attr.required) {
1131             plv8.elog(ERROR, "Attribute " + ormp.name + " is required.");
1132           }
1133         }
1134       }
1135
1136       if (!isValidSql) {
1137         return false;
1138       }
1139
1140       /* Build the insert statement */
1141       columns = params.columns.join(', ');
1142       columns = XT.format(columns, params.identifiers);
1143       expressions = params.expressions.join(', ');
1144       expressions = XT.format(expressions, params.identifiers);
1145
1146       if (params.table.indexOf(".") > 0) {
1147         namespace = params.table.beforeDot();
1148         table = params.table.afterDot();
1149         query = 'insert into %1$I.%2$I (' + columns + ') values (' + expressions + ')';
1150         params.statement = XT.format(query, [namespace, table]);
1151       } else {
1152         query = 'insert into %1$I (' + columns + ') values (' + expressions + ')';
1153         params.statement = XT.format(query, [params.table]);
1154       }
1155
1156       /* If we can get the primary key column we want to return that
1157          for cases where it is determined behind the scenes */
1158       if (!record[pkey] && !params.primaryKey) {
1159         params.primaryKey = XT.Orm.primaryKey(orm, true);
1160       }
1161
1162       if (params.primaryKey && params.parentUuid) {
1163         params.statement = params.statement + ' returning ' + params.primaryKey + ' as id, obj_uuid';
1164       } else if (params.parentUuid) {
1165         params.statement = params.statement + ' returning obj_uuid';
1166       } else if (params.primaryKey) {
1167         params.statement = params.statement + ' returning ' + params.primaryKey + ' as id';
1168       }
1169
1170       if (DEBUG) {
1171         XT.debug('prepareInsert statement =', params.statement);
1172         XT.debug('prepareInsert values =', params.values);
1173       }
1174
1175       return params;
1176     },
1177
1178     /**
1179      * Commit update to the database
1180      *
1181      * @param {Object} Options
1182      * @param {String} [options.nameSpace] Namespace. Required.
1183      * @param {String} [options.type] Type. Required.
1184      * @param {Object} [options.data] The data payload to be processed. Required.
1185      * @param {Number} [options.etag] Record version for optimistic locking.
1186      * @param {Object} [options.lock] Lock information for pessemistic locking.
1187      * @param {String} [options.encryptionKey] Encryption key.
1188      */
1189     updateRecord: function (options) {
1190       var data = options.data,
1191         encryptionKey = options.encryptionKey,
1192         orm = this.fetchOrm(options.nameSpace, options.type),
1193         pkey = XT.Orm.primaryKey(orm),
1194         id = data[pkey],
1195         ext,
1196         etag = this.getVersion(orm, id),
1197         i,
1198         iORuQuery,
1199         iORuSql,
1200         lock,
1201         lockKey = options.lock && options.lock.key ? options.lock.key : false,
1202         lockTable = orm.lockTable || orm.table,
1203         rows,
1204         sql = this.prepareUpdate(orm, data, null, encryptionKey);
1205
1206       /* Test for optimistic lock. */
1207       if (!XT.disableLocks && etag && options.etag !== etag) {
1208       // TODO - Improve error handling.
1209         plv8.elog(ERROR, "The version being updated is not current.");
1210       }
1211       /* Test for pessimistic lock. */
1212       if (orm.lockable) {
1213         lock = this.tryLock(lockTable, id, {key: lockKey});
1214         if (!lock.key) {
1215           // TODO - Improve error handling.
1216           plv8.elog(ERROR, "Can not obtain a lock on the record.");
1217         }
1218       }
1219
1220       /* Okay, now lets handle arrays. */
1221       this.commitArrays(orm, data, encryptionKey);
1222
1223       /* Handle extensions on the same table. */
1224       for (var i = 0; i < orm.extensions.length; i++) {
1225         if (orm.extensions[i].table === orm.table) {
1226           sql = this.prepareUpdate(orm.extensions[i], data, sql, encryptionKey);
1227         }
1228       }
1229
1230       sql.values.push(id);
1231
1232       /* Commit the base record. */
1233       if (DEBUG) {
1234         XT.debug('updateRecord sql =', sql.statement);
1235         XT.debug('updateRecord values =', sql.values);
1236       }
1237       plv8.execute(sql.statement, sql.values);
1238
1239       /* Handle extensions on other tables. */
1240       for (var i = 0; i < orm.extensions.length; i++) {
1241         ext = orm.extensions[i];
1242         if (ext.table !== orm.table &&
1243            !ext.isChild) {
1244
1245           /* Determine whether to insert or update. */
1246           if (ext.table.indexOf(".") > 0) {
1247             iORuQuery = "select %1$I from %2$I.%3$I where %1$I = $1;";
1248             iORuSql = XT.format(iORuQuery, [
1249                 ext.relations[0].column,
1250                 ext.table.beforeDot(),
1251                 ext.table.afterDot()
1252               ]);
1253           } else {
1254             iORuQuery = "select %1$I from %2$I where %1$I = $1;";
1255             iORuSql = XT.format(iORuQuery, [ext.relations[0].column, ext.table]);
1256           }
1257
1258           if (DEBUG) {
1259             XT.debug('updateRecord sql =', iORuSql);
1260             XT.debug('updateRecord values =', [data[pkey]]);
1261           }
1262           rows = plv8.execute(iORuSql, [data[pkey]]);
1263
1264           if (rows.length) {
1265             sql = this.prepareUpdate(ext, data, null, encryptionKey);
1266             sql.values.push(id);
1267           } else {
1268             sql = this.prepareInsert(ext, data, null, encryptionKey);
1269           }
1270
1271           if (DEBUG) {
1272             XT.debug('updateRecord sql =', sql.statement);
1273             XT.debug('updateRecord values =', sql.values);
1274           }
1275
1276           if (sql.statement) {
1277             plv8.execute(sql.statement, sql.values);
1278           }
1279         }
1280       }
1281
1282       /* Release any lock. */
1283       if (orm.lockable) {
1284         this.releaseLock({table: lockTable, id: id});
1285       }
1286     },
1287
1288     /**
1289      * Use an orm object and a record and build an update statement. It
1290      * returns an object with a table name string, expressions array and
1291      * insert statement string that can be executed.
1292      *
1293      * The optional params object includes objects columns, expressions
1294      * that can be cumulatively added to the result.
1295      *
1296      * @params {Object} Orm
1297      * @params {Object} Record
1298      * @params {Object} Params - optional
1299      * @returns {Object}
1300      */
1301     prepareUpdate: function (orm, record, params, encryptionKey) {
1302       var attr,
1303         attributePrivileges,
1304         columnKey,
1305         count,
1306         encryptQuery,
1307         encryptSql,
1308         exp,
1309         expressions,
1310         iorm,
1311         key,
1312         keyValue,
1313         namespace,
1314         ormp,
1315         pkey,
1316         prop,
1317         query,
1318         table,
1319         toOneQuery,
1320         toOneSql,
1321         type,
1322         val,
1323         isValidSql = false,
1324         canEdit;
1325
1326       params = params || {
1327         table: "",
1328         expressions: [],
1329         identifiers: [],
1330         values: []
1331       };
1332       params.table = orm.table;
1333       count = params.values.length + 1;
1334
1335       if (orm.relations) {
1336         /* Extension. */
1337         pkey = orm.relations[0].inverse;
1338         columnKey = orm.relations[0].column;
1339       } else {
1340         /* Base. */
1341         pkey = XT.Orm.primaryKey(orm);
1342         columnKey = XT.Orm.primaryKey(orm, true);
1343       }
1344
1345       /* Build up the content for update of this record. */
1346       for (var i = 0; i < orm.properties.length; i++) {
1347         ormp = orm.properties[i];
1348         prop = ormp.name;
1349         attr = ormp.attr ? ormp.attr : ormp.toOne ? ormp.toOne : ormp.toMany;
1350         type = attr.type;
1351         iorm = ormp.toOne ? this.fetchOrm(orm.nameSpace, ormp.toOne.type) : false;
1352         nkey = iorm ? XT.Orm.naturalKey(iorm, true) : false;
1353         val = ormp.toOne && record[prop] instanceof Object ?
1354           record[prop][nkey || ormp.toOne.inverse || 'id'] : record[prop],
1355
1356         attributePrivileges = orm.privileges &&
1357           orm.privileges.attribute &&
1358           orm.privileges.attribute[prop];
1359
1360         /**
1361          * Ignore derived fields for insert/update
1362          */
1363         if (attr.derived) continue;
1364
1365         if(!attributePrivileges || attributePrivileges.update === undefined) {
1366           canEdit = true;
1367         } else if (typeof attributePrivileges.update === 'string') {
1368           canEdit = this.checkPrivilege(attributePrivileges.update);
1369         } else {
1370           canEdit = attributePrivileges.update; /* if it's true or false */
1371         }
1372
1373         if (canEdit && val !== undefined && !ormp.toMany) {
1374
1375           /* Handle encryption if applicable. */
1376           if (attr.isEncrypted) {
1377             if (encryptionKey) {
1378               encryptQuery = "select encrypt(setbytea(%1$L), setbytea(%2$L), %3$L)";
1379               encryptSql = XT.format(encryptQuery, [val, encryptionKey, 'bf']);
1380               val = record[prop] ? plv8.execute(encryptSql)[0].encrypt : null;
1381               params.values.push(val);
1382               params.identifiers.push(attr.column);
1383               params.expressions.push("%" + count + "$I = $" + count);
1384               isValidSql = true;
1385               count++;
1386             } else {
1387               // TODO - Improve error handling.
1388               throw new Error("No encryption key provided.");
1389             }
1390           } else if (ormp.name !== pkey) {
1391             if (val === null) {
1392               if (attr.required) {
1393                 plv8.elog(ERROR, "Attribute " + ormp.name + " is required.");
1394               } else {
1395                 params.values.push(attr.nullValue || null);
1396                 params.expressions.push("%" + count + "$I = $" + count);
1397               }
1398             } else if (ormp.toOne && nkey) {
1399               if (iorm.table.indexOf(".") > 0) {
1400                 toOneQuery = "select %1$I from %2$I.%3$I where %4$I = $" + count;
1401                 toOneSql = XT.format(toOneQuery, [
1402                     XT.Orm.primaryKey(iorm, true),
1403                     iorm.table.beforeDot(),
1404                     iorm.table.afterDot(),
1405                     nkey
1406                   ]);
1407               } else {
1408                 toOneQuery = "select %1$I from %2$I where %3$I = $" + count;
1409                 toOneSql = XT.format(toOneQuery, [
1410                     XT.Orm.primaryKey(iorm, true),
1411                     iorm.table,
1412                     nkey
1413                   ]);
1414               }
1415
1416               exp = "%" + count + "$I = (" + toOneSql + ")";
1417               params.values.push(val);
1418               params.expressions.push(exp);
1419             } else {
1420               params.values.push(val);
1421               params.expressions.push("%" + count + "$I = $" + count);
1422             }
1423             params.identifiers.push(attr.column);
1424             isValidSql = true;
1425             count++;
1426           }
1427         }
1428       }
1429
1430       /* Build the update statement */
1431       expressions = params.expressions.join(', ');
1432       expressions = XT.format(expressions, params.identifiers);
1433
1434       // do not send an invalid sql statement
1435       if (!isValidSql) { return params; }
1436
1437       if (params.table.indexOf(".") > 0) {
1438         namespace = params.table.beforeDot();
1439         table = params.table.afterDot();
1440         query = 'update %1$I.%2$I set ' + expressions + ' where %3$I = $' + count + ';';
1441         params.statement = XT.format(query, [namespace, table, columnKey]);
1442       } else {
1443         query = 'update %1$I set ' + expressions + ' where %2$I = $' + count + ';';
1444         params.statement = XT.format(query, [params.table, columnKey]);
1445       }
1446
1447       if (DEBUG) {
1448         XT.debug('prepareUpdate statement =', params.statement);
1449         XT.debug('prepareUpdate values =', params.values);
1450       }
1451
1452       return params;
1453     },
1454
1455     /**
1456      * Commit deletion to the database
1457      *
1458      * @param {Object} Options
1459      * @param {String} [options.nameSpace] Namespace. Required.
1460      * @param {String} [options.type] Type. Required.
1461      * @param {Object} [options.data] The data payload to be processed. Required.
1462      * @param {Number} [options.etag] Optional record id version for optimistic locking.
1463      *  If set and version does not match, delete will fail.
1464      * @param {Number} [options.lock] Lock information for pessemistic locking.
1465      */
1466     deleteRecord: function (options) {
1467       var data = options.data,
1468         orm = this.fetchOrm(options.nameSpace, options.type, {silentError: true}),
1469         pkey,
1470         nkey,
1471         id,
1472         columnKey,
1473         etag,
1474         ext,
1475         i,
1476         lockKey = options.lock && options.lock.key ? options.lock.key : false,
1477         lockTable,
1478         namespace,
1479         prop,
1480         ormp,
1481         query = '',
1482         sql = '',
1483         table,
1484         values;
1485
1486       /* Set variables or return false with message. */
1487       if (!orm) {
1488         throw new handleError("Not Found", 404);
1489       }
1490
1491       pkey = XT.Orm.primaryKey(orm);
1492       nkey = XT.Orm.naturalKey(orm);
1493       lockTable = orm.lockTable || orm.table;
1494       if (!pkey && !nkey) {
1495         throw new handleError("Not Found", 404);
1496       }
1497
1498       id = nkey ? this.getId(orm, data[nkey]) : data[pkey];
1499       if (!id) {
1500         throw new handleError("Not Found", 404);
1501       }
1502
1503       /* Test for optional optimistic lock. */
1504       etag = this.getVersion(orm, id);
1505       if (etag && options.etag && etag !== options.etag) {
1506         throw new handleError("Precondition Required", 428);
1507       }
1508
1509       /* Test for pessemistic lock. */
1510       if (orm.lockable) {
1511         lock = this.tryLock(lockTable, id, {key: lockKey});
1512         if (!lock.key) {
1513           throw new handleError("Conflict", 409);
1514         }
1515       }
1516
1517       /* Delete children first. */
1518       for (prop in data) {
1519         ormp = XT.Orm.getProperty(orm, prop);
1520
1521         /* If the property is an array of objects they must be records so delete them. */
1522         if (ormp.toMany && ormp.toMany.isNested) {
1523           values = data[prop];
1524           for (var i = 0; i < values.length; i++) {
1525             this.deleteRecord({
1526               nameSpace: options.nameSpace,
1527               type: ormp.toMany.type,
1528               data: values[i]
1529             });
1530           }
1531         }
1532       }
1533
1534       /* Next delete from extension tables. */
1535       for (var i = 0; i < orm.extensions.length; i++) {
1536         ext = orm.extensions[i];
1537         if (ext.table !== orm.table &&
1538             !ext.isChild) {
1539           columnKey = ext.relations[0].column;
1540           nameKey = ext.relations[0].inverse;
1541
1542           if (ext.table.indexOf(".") > 0) {
1543             namespace = ext.table.beforeDot();
1544             table = ext.table.afterDot();
1545             query = 'delete from %1$I.%2$I where %3$I = $1';
1546             sql = XT.format(query, [namespace, table, columnKey]);
1547           } else {
1548             query = 'delete from %1$I where %2$I = $1';
1549             sql = XT.format(query, [ext.table, columnKey]);
1550           }
1551
1552           if (DEBUG) {
1553             XT.debug('deleteRecord sql =', sql);
1554             XT.debug('deleteRecord values =',  [id]);
1555           }
1556           plv8.execute(sql, [id]);
1557         }
1558       }
1559
1560       /* Now delete the top. */
1561       nameKey = XT.Orm.primaryKey(orm);
1562       columnKey = XT.Orm.primaryKey(orm, true);
1563
1564       if (orm.table.indexOf(".") > 0) {
1565         namespace = orm.table.beforeDot();
1566         table = orm.table.afterDot();
1567         query = 'delete from %1$I.%2$I where %3$I = $1';
1568         sql = XT.format(query, [namespace, table, columnKey]);
1569       } else {
1570         query = 'delete from %1$I where %2$I = $1';
1571         sql = XT.format(query, [orm.table, columnKey]);
1572       }
1573
1574       /* Commit the record.*/
1575       if (DEBUG) {
1576         XT.debug('deleteRecord sql =', sql);
1577         XT.debug('deleteRecord values =', [id]);
1578       }
1579       plv8.execute(sql, [id]);
1580
1581       /* Release any lock. */
1582       if (orm.lockable) {
1583         this.releaseLock({table: lockTable, id: id});
1584       }
1585     },
1586
1587     /**
1588      * Decrypts properties where applicable.
1589      *
1590      * @param {String} name space
1591      * @param {String} type
1592      * @param {Object} record
1593      * @param {Object} encryption key
1594      * @returns {Object}
1595      */
1596     decrypt: function (nameSpace, type, record, encryptionKey) {
1597       var result,
1598         that = this,
1599         hexToAlpha = function (hex) {
1600           var str = '', i;
1601           for (i = 2; i < hex.length; i += 2) {
1602             str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
1603           }
1604           return str;
1605         },
1606         orm = this.fetchOrm(nameSpace, type);
1607
1608       for (prop in record) {
1609         var ormp = XT.Orm.getProperty(orm, prop.camelize());
1610
1611         /* Decrypt property if applicable. */
1612         if (ormp && ormp.attr && ormp.attr.isEncrypted) {
1613           if (encryptionKey) {
1614             sql = "select formatbytea(decrypt($1, setbytea($2), 'bf')) as result";
1615             // TODO - Handle not found error.
1616
1617             if (DEBUG && false) {
1618               XT.debug('decrypt prop =', prop);
1619               XT.debug('decrypt sql =', sql);
1620               XT.debug('decrypt values =', [record[prop], encryptionKey]);
1621             }
1622             result = plv8.execute(sql, [record[prop], encryptionKey])[0].result;
1623             /* we SOMETIMES need to translate from hex here */
1624             if(typeof result === 'string' && result.substring(0, 2) === '\\x') {
1625               result = result ? hexToAlpha(result) : result;
1626             }
1627             /* in the special case of encrypted credit card numbers, we don't give the
1628               user the full decrypted number EVEN IF they have the encryption key */
1629             if(ormp.attr.isEncrypted === "credit_card_number" && result && result.length >= 4) {
1630               record[prop] = "************" + result.substring(result.length - 4);
1631             } else {
1632               record[prop] = result;
1633             }
1634           } else {
1635             record[prop] = '**********';
1636           }
1637
1638         /* Check recursively. */
1639         } else if (ormp.toOne && ormp.toOne.isNested) {
1640           that.decrypt(nameSpace, ormp.toOne.type, record[prop], encryptionKey);
1641
1642         } else if (ormp.toMany && ormp.toMany.isNested) {
1643           record[prop].map(function (subdata) {
1644             that.decrypt(nameSpace, ormp.toMany.type, subdata, encryptionKey);
1645           });
1646         }
1647       }
1648
1649       return record;
1650     },
1651
1652     /**
1653       Fetches the ORM. Caches the result in this data object, where it can be used
1654       for this request but will be conveniently forgotten between requests.
1655      */
1656     fetchOrm: function (nameSpace, type) {
1657       var res,
1658         ret,
1659         recordType = nameSpace + '.'+ type;
1660
1661       if (!this._maps) {
1662         this._maps = [];
1663       }
1664
1665       res = this._maps.findProperty('recordType', recordType);
1666       if (res) {
1667         ret = res.map;
1668       } else {
1669         ret = XT.Orm.fetch(nameSpace, type);
1670
1671         /* cache the result so we don't requery needlessly */
1672         this._maps.push({ "recordType": recordType, "map": ret});
1673       }
1674       return ret;
1675     },
1676
1677     /**
1678      * Get the oid for a given table name.
1679      *
1680      * @param {String} table name
1681      * @returns {Number}
1682      */
1683     getTableOid: function (table) {
1684       var tableName = this.getTableFromNamespacedTable(table).toLowerCase(), /* be generous */
1685         namespace = this.getNamespaceFromNamespacedTable(table),
1686         ret,
1687         sql = "select pg_class.oid::integer as oid " +
1688              "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
1689              "where relname = $1 and nspname = $2";
1690
1691       if (DEBUG) {
1692         XT.debug('getTableOid sql =', sql);
1693         XT.debug('getTableOid values =', [tableName, namespace]);
1694       }
1695       ret = plv8.execute(sql, [tableName, namespace])[0].oid - 0;
1696
1697       // TODO - Handle not found error.
1698
1699       return ret;
1700     },
1701
1702     /**
1703      * Get the primary key id for an object based on a passed in natural key.
1704      *
1705      * @param {Object} Orm
1706      * @param {String} Natural key value
1707      */
1708     getId: function (orm, value) {
1709       var ncol = XT.Orm.naturalKey(orm, true),
1710         pcol = XT.Orm.primaryKey(orm, true),
1711         query,
1712         ret,
1713         sql;
1714
1715       if (orm.table.indexOf(".") > 0) {
1716         namespace = orm.table.beforeDot();
1717         table = orm.table.afterDot();
1718         query = "select %1$I as id from %2$I.%3$I where %4$I = $1";
1719         sql = XT.format(query, [pcol, namespace, table, ncol]);
1720       } else {
1721         query = "select %1$I as id from %2$I where %3$I = $1";
1722         sql = XT.format(query, [pcol, orm.table, ncol]);
1723       }
1724
1725       if (DEBUG) {
1726         XT.debug('getId sql =', sql);
1727         XT.debug('getId values =', [value]);
1728       }
1729
1730       ret = plv8.execute(sql, [value]);
1731
1732       if(ret.length) {
1733         return ret[0].id;
1734       } else {
1735         throw new handleError("Primary Key not found on " + orm.table +
1736           " where " + ncol + " = " + value, 400);
1737       }
1738     },
1739
1740     getNamespaceFromNamespacedTable: function (fullName) {
1741       return fullName.indexOf(".") > 0 ? fullName.beforeDot() : "public";
1742     },
1743
1744     getTableFromNamespacedTable: function (fullName) {
1745       return fullName.indexOf(".") > 0 ? fullName.afterDot() : fullName;
1746     },
1747
1748     getPgTypeFromOrmType: function (schema, table, column) {
1749       var sql = "select data_type from information_schema.columns " +
1750                 "where true " +
1751                 "and table_schema = $1 " +
1752                 "and table_name = $2 " +
1753                 "and column_name = $3;",
1754           pgType,
1755           values = [schema, table, column];
1756
1757       if (DEBUG) {
1758         XT.debug('getPgTypeFromOrmType sql =', sql);
1759         XT.debug('getPgTypeFromOrmType values =', values);
1760       }
1761
1762       pgType = plv8.execute(sql, values);
1763       pgType = pgType && pgType[0] ? pgType[0].data_type : false;
1764
1765       return pgType;
1766     },
1767
1768     /**
1769      * Get the natural key id for an object based on a passed in primary key.
1770      *
1771      * @param {Object} Orm
1772      * @param {Number|String} Primary key value
1773      * @param {Boolean} safe Return the original value instead of erroring if no match is found
1774      */
1775     getNaturalId: function (orm, value, safe) {
1776       var ncol = XT.Orm.naturalKey(orm, true),
1777         pcol = XT.Orm.primaryKey(orm, true),
1778         query,
1779         ret,
1780         sql;
1781
1782       if (orm.table.indexOf(".") > 0) {
1783         namespace = orm.table.beforeDot();
1784         table = orm.table.afterDot();
1785         query = "select %1$I as id from %2$I.%3$I where %4$I = $1";
1786         sql = XT.format(query, [ncol, namespace, table, pcol]);
1787       } else {
1788         query = "select %1$I as id from %2$I where %3$I = $1";
1789         sql = XT.format(query, [ncol, orm.table, pcol]);
1790       }
1791
1792       if (DEBUG) {
1793         XT.debug('getNaturalId sql =', sql);
1794         XT.debug('getNaturalId values =', [value]);
1795       }
1796
1797       ret = plv8.execute(sql, [value]);
1798
1799       if (ret.length) {
1800         return ret[0].id;
1801       } else if (safe) {
1802         return value;
1803       } else {
1804         throw new handleError("Natural Key Not Found: " + orm.nameSpace + "." + orm.type, 400);
1805       }
1806     },
1807
1808     /**
1809      * Returns the current version of a record.
1810      *
1811      * @param {Object} Orm
1812      * @param {Number|String} Record id
1813      */
1814     getVersion: function (orm, id) {
1815       if (!orm.lockable) { return; }
1816
1817       var etag,
1818         oid = this.getTableOid(orm.lockTable || orm.table),
1819         res,
1820         sql = 'select ver_etag from xt.ver where ver_table_oid = $1 and ver_record_id = $2;';
1821
1822       if (DEBUG) {
1823         XT.debug('getVersion sql = ', sql);
1824         XT.debug('getVersion values = ', [oid, id]);
1825       }
1826       res = plv8.execute(sql, [oid, id]);
1827       etag = res.length ? res[0].ver_etag : false;
1828
1829       if (!etag) {
1830         etag = XT.generateUUID();
1831         sql = 'insert into xt.ver (ver_table_oid, ver_record_id, ver_etag) values ($1, $2, $3::uuid);';
1832         // TODO - Handle insert error.
1833
1834         if (DEBUG) {
1835           XT.debug('getVersion insert sql = ', sql);
1836           XT.debug('getVersion insert values = ', [oid, id, etag]);
1837         }
1838         plv8.execute(sql, [oid, id, etag]);
1839       }
1840
1841       return etag;
1842     },
1843
1844     /**
1845      * Fetch an array of records from the database.
1846      *
1847      * @param {Object} Options
1848      * @param {String} [dataHash.nameSpace] Namespace. Required.
1849      * @param {String} [dataHash.type] Type. Required.
1850      * @param {Array} [dataHash.parameters] Parameters
1851      * @param {Array} [dataHash.orderBy] Order by - optional
1852      * @param {Number} [dataHash.rowLimit] Row limit - optional
1853      * @param {Number} [dataHash.rowOffset] Row offset - optional
1854      * @returns Array
1855      */
1856     fetch: function (options) {
1857       var nameSpace = options.nameSpace,
1858         type = options.type,
1859         query = options.query || {},
1860         encryptionKey = options.encryptionKey,
1861         orderBy = query.orderBy,
1862         orm = this.fetchOrm(nameSpace, type),
1863         table,
1864         tableNamespace,
1865         parameters = query.parameters,
1866         clause = this.buildClauseOptimized(nameSpace, type, parameters, orderBy),
1867         i,
1868         pkey = XT.Orm.primaryKey(orm),
1869         pkeyColumn = XT.Orm.primaryKey(orm, true),
1870         nkey = XT.Orm.naturalKey(orm),
1871         limit = query.rowLimit ? XT.format('limit %1$L', [query.rowLimit]) : '',
1872         offset = query.rowOffset ? XT.format('offset %1$L', [query.rowOffset]) : '',
1873         parts,
1874         ret = {
1875           nameSpace: nameSpace,
1876           type: type
1877         },
1878         qry,
1879         ids = [],
1880         idParams = [],
1881         counter = 1,
1882         sqlCount,
1883         etags,
1884         sql_etags,
1885         sql1 = 'select t1.%3$I as id from %1$I.%2$I t1 {joins} where {conditions} group by t1.%3$I{groupBy} {orderBy} {limit} {offset};',
1886         sql2 = 'select * from %1$I.%2$I where %3$I in ({ids}) {orderBy}';
1887
1888       /* Validate - don't bother running the query if the user has no privileges. */
1889       if (!this.checkPrivileges(nameSpace, type)) { return []; }
1890
1891       tableNamespace = this.getNamespaceFromNamespacedTable(orm.table);
1892       table = this.getTableFromNamespacedTable(orm.table);
1893
1894       if (query.count) {
1895         /* Just get the count of rows that match the conditions */
1896         sqlCount = 'select count(distinct t1.%3$I) as count from %1$I.%2$I t1 {joins} where {conditions};';
1897         sqlCount = XT.format(sqlCount, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
1898         sqlCount = sqlCount.replace('{joins}', clause.joins)
1899                            .replace('{conditions}', clause.conditions);
1900
1901         if (DEBUG) {
1902           XT.debug('fetch sqlCount = ', sqlCount);
1903           XT.debug('fetch values = ', clause.parameters);
1904         }
1905
1906         ret.data = plv8.execute(sqlCount, clause.parameters);
1907         return ret;
1908       }
1909
1910       /* Query the model. */
1911       sql1 = XT.format(sql1, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
1912       sql1 = sql1.replace('{joins}', clause.joins)
1913                  .replace('{conditions}', clause.conditions)
1914                  .replace(/{groupBy}/g, clause.groupByColumns)
1915                  .replace(/{orderBy}/g, clause.orderByColumns)
1916                  .replace('{limit}', limit)
1917                  .replace('{offset}', offset);
1918
1919       if (DEBUG) {
1920         XT.debug('fetch sql1 = ', sql1);
1921         XT.debug('fetch values = ', clause.parameters);
1922       }
1923
1924       /* First query for matching ids, then get entire result set. */
1925       /* This improves performance over a direct query on the view due */
1926       /* to the way sorting is handled by the query optimizer */
1927       qry = plv8.execute(sql1, clause.parameters) || [];
1928       if (!qry.length) { return [] };
1929       qry.forEach(function (row) {
1930         ids.push(row.id);
1931         idParams.push("$" + counter);
1932         counter++;
1933       });
1934
1935       if (orm.lockable) {
1936         sql_etags = "select ver_etag as etag, ver_record_id as id " +
1937                     "from xt.ver " +
1938                     "where ver_table_oid = ( " +
1939                       "select pg_class.oid::integer as oid " +
1940                       "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
1941                       /* Note: using $L for quoted literal e.g. 'contact', not an identifier. */
1942                       "where nspname = %1$L and relname = %2$L " +
1943                     ") " +
1944                     "and ver_record_id in ({ids})";
1945         sql_etags = XT.format(sql_etags, [tableNamespace, table]);
1946         sql_etags = sql_etags.replace('{ids}', idParams.join());
1947
1948         if (DEBUG) {
1949           XT.debug('fetch sql_etags = ', sql_etags);
1950           XT.debug('fetch etags_values = ', JSON.stringify(ids));
1951         }
1952         etags = plv8.execute(sql_etags, ids) || {};
1953         ret.etags = {};
1954       }
1955
1956       sql2 = XT.format(sql2, [nameSpace.decamelize(), type.decamelize(), pkey]);
1957       sql2 = sql2.replace(/{orderBy}/g, clause.orderBy)
1958                  .replace('{ids}', idParams.join());
1959
1960       if (DEBUG) {
1961         XT.debug('fetch sql2 = ', sql2);
1962         XT.debug('fetch values = ', JSON.stringify(ids));
1963       }
1964       ret.data = plv8.execute(sql2, ids) || [];
1965
1966       for (var i = 0; i < ret.data.length; i++) {
1967         ret.data[i] = this.decrypt(nameSpace, type, ret.data[i], encryptionKey);
1968
1969         if (etags) {
1970           /* Add etags to result in pkey->etag format. */
1971           for (var j = 0; j < etags.length; j++) {
1972             if (etags[j].id === ret.data[i][pkey]) {
1973               ret.etags[ret.data[i][nkey]] = etags[j].etag;
1974             }
1975           }
1976         }
1977       }
1978
1979       this.sanitize(nameSpace, type, ret.data, options);
1980
1981       return ret;
1982     },
1983
1984     /**
1985     Fetch a metric value.
1986
1987     @param {String} Metric name
1988     @param {String} Return type 'text', 'boolean' or 'number' (default 'text')
1989     */
1990     fetchMetric: function (name, type) {
1991       var fn = 'fetchmetrictext';
1992       if (type === 'boolean') {
1993         fn = 'fetchmetricbool';
1994       } else if (type === 'number') {
1995         fn = 'fetchmetricvalue';
1996       }
1997       return plv8.execute("select " + fn + "($1) as resp", [name])[0].resp;
1998     },
1999
2000     /**
2001      * Retreives a record from the database. If the user does not have appropriate privileges an
2002      * error will be thrown unless the `silentError` option is passed.
2003      *
2004      * If `context` is passed as an option then a record will only be returned if it exists in the context (parent)
2005      * record which itself must be accessible by the effective user.
2006      *
2007      * @param {Object} options
2008      * @param {String} [options.nameSpace] Namespace. Required.
2009      * @param {String} [options.type] Type. Required.
2010      * @param {Number} [options.id] Record id. Required.
2011      * @param {Boolean} [options.superUser=false] If true ignore privilege checking.
2012      * @param {String} [options.encryptionKey] Encryption key
2013      * @param {Boolean} [options.silentError=false] Silence errors
2014      * @param {Object} [options.context] Context
2015      * @param {String} [options.context.nameSpace] Context namespace.
2016      * @param {String} [options.context.type] The type of context object.
2017      * @param {String} [options.context.value] The value of the context's primary key.
2018      * @param {String} [options.context.relation] The name of the attribute on the type to which this record is related.
2019      * @returns Object
2020      */
2021     retrieveRecord: function (options) {
2022       options = options ? options : {};
2023       options.obtainLock = false;
2024
2025       var id = options.id,
2026         nameSpace = options.nameSpace,
2027         type = options.type,
2028         map = this.fetchOrm(nameSpace, type),
2029         context = options.context,
2030         encryptionKey = options.encryptionKey,
2031         join = "",
2032         lockTable = map.lockTable || map.table,
2033         nkey = XT.Orm.naturalKey(map),
2034         params = {},
2035         pkey = XT.Orm.primaryKey(map),
2036         ret = {
2037           nameSpace: nameSpace,
2038           type: type,
2039           id: id
2040         },
2041         sql;
2042
2043       if (!pkey) {
2044         throw new Error('No key found for {nameSpace}.{type}'
2045                         .replace("{nameSpace}", nameSpace)
2046                         .replace("{type}", type));
2047       }
2048
2049       /* If this object uses a natural key, go get the primary key id. */
2050       if (nkey) {
2051         id = this.getId(map, id);
2052         if (!id) {
2053           return false;
2054         }
2055       }
2056
2057       /* Context means search for this record inside another. */
2058       if (context) {
2059         context.nameSpace = context.nameSpace || context.recordType.beforeDot();
2060         context.type = context.type || context.recordType.afterDot()
2061         context.map = this.fetchOrm(context.nameSpace, context.type);
2062         context.prop = XT.Orm.getProperty(context.map, context.relation);
2063         context.pertinentExtension = XT.Orm.getProperty(context.map, context.relation, true);
2064         context.underlyingTable = context.pertinentExtension.table,
2065         context.underlyingNameSpace = this.getNamespaceFromNamespacedTable(context.underlyingTable);
2066         context.underlyingType = this.getTableFromNamespacedTable(context.underlyingTable);
2067         context.fkey = context.prop.toMany.inverse;
2068         context.fkeyColumn = context.prop.toMany.column;
2069         context.pkey = XT.Orm.naturalKey(context.map) || XT.Orm.primaryKey(context.map);
2070         params.attribute = context.pkey;
2071         params.value = context.value;
2072
2073         join = 'join %1$I.%2$I on (%1$I.%2$I.%3$I = %4$I.%5$I)';
2074         join = XT.format(join, [
2075             context.underlyingNameSpace,
2076             context.underlyingType,
2077             context.fkeyColumn,
2078             type.decamelize(),
2079             context.fkey
2080           ]);
2081       }
2082
2083       /* Validate - don't bother running the query if the user has no privileges. */
2084       if(!options.superUser && !context && !this.checkPrivileges(nameSpace, type)) {
2085         if (options.silentError) {
2086           return false;
2087         } else {
2088           throw new handleError("Unauthorized", 401);
2089         }
2090       }
2091
2092       ret.etag = this.getVersion(map, id);
2093
2094       /* Obtain lock if required. */
2095       if (map.lockable) {
2096         ret.lock = this.tryLock(lockTable, id, options);
2097       }
2098
2099       /* Data sql. */
2100       sql = 'select %1$I.* from %2$I.%1$I {join} where %1$I.%3$I = $1;';
2101       sql = sql.replace(/{join}/, join);
2102       sql = XT.format(sql, [type.decamelize(), nameSpace.decamelize(), pkey]);
2103
2104       /* Query the map. */
2105       if (DEBUG) {
2106         XT.debug('retrieveRecord sql = ', sql);
2107         XT.debug('retrieveRecord values = ', [id]);
2108       }
2109       ret.data = plv8.execute(sql, [id])[0] || {};
2110
2111       if (!context) {
2112         /* Check privileges again, this time against record specific criteria where applicable. */
2113         if(!options.superUser && !this.checkPrivileges(nameSpace, type, ret.data)) {
2114           if (options.silentError) {
2115             return false;
2116           } else {
2117             throw new handleError("Unauthorized", 401);
2118           }
2119         }
2120         /* Decrypt result where applicable. */
2121         ret.data = this.decrypt(nameSpace, type, ret.data, encryptionKey);
2122       }
2123
2124       this.sanitize(nameSpace, type, ret.data, options);
2125
2126       /* Return the results. */
2127       return ret || {};
2128     },
2129
2130     /**
2131      *  Remove unprivileged attributes, primary and foreign keys from the data.
2132      *  Only removes the primary key if a natural key has been specified in the ORM.
2133      *  Also format for printing using XT.format functions if printFormat=true'
2134      *
2135      * @param {String} Namespace
2136      * @param {String} Type
2137      * @param {Object|Array} Data
2138      * @param {Object} Options
2139      * @param {Boolean} [options.includeKeys=false] Do not remove primary and foreign keys.
2140      * @param {Boolean} [options.superUser=false] Do not remove unprivileged attributes.
2141      * @param {Boolean} [options.printFormat=true] Format for printing.
2142      */
2143     sanitize: function (nameSpace, type, data, options) {
2144       options = options || {};
2145       if (options.includeKeys && options.superUser) { return; }
2146       if (XT.typeOf(data) !== "array") { data = [data]; }
2147       var orm = this.fetchOrm(nameSpace, type),
2148         pkey = XT.Orm.primaryKey(orm),
2149         nkey = XT.Orm.naturalKey(orm),
2150         props = orm.properties,
2151         attrPriv = orm.privileges && orm.privileges.attribute ?
2152           orm.privileges.attribute : false,
2153         inclKeys = options.includeKeys,
2154         superUser = options.superUser,
2155         printFormat = options.printFormat,
2156         c,
2157         i,
2158         item,
2159         n,
2160         prop,
2161         itemAttr,
2162         filteredProps,
2163         val,
2164         preOffsetDate,
2165         offsetDate,
2166         check = function (p) {
2167           return p.name === itemAttr;
2168         };
2169
2170       for (var c = 0; c < data.length; c++) {
2171         item = data[c];
2172
2173         /* Remove primary key if applicable */
2174         if (!inclKeys && nkey && nkey !== pkey) { delete item[pkey]; }
2175
2176         for (itemAttr in item) {
2177           if (!item.hasOwnProperty(itemAttr)) {
2178             continue;
2179           }
2180           filteredProps = orm.properties.filter(check);
2181
2182           if (filteredProps.length === 0 && orm.extensions.length > 0) {
2183             /* Try to get the orm prop from an extension if it's not in the core*/
2184             orm.extensions.forEach(function (ext) {
2185               if (filteredProps.length === 0) {
2186                 filteredProps = ext.properties.filter(check);
2187               }
2188             });
2189           }
2190
2191           /* Remove attributes not found in the ORM */
2192           if (filteredProps.length === 0) {
2193             delete item[itemAttr];
2194           } else {
2195             prop = filteredProps[0];
2196           }
2197
2198           /* Remove unprivileged attribute if applicable */
2199           if (!superUser && attrPriv && attrPriv[prop.name] &&
2200             (attrPriv[prop.name].view !== undefined) &&
2201             !this.checkPrivilege(attrPriv[prop.name].view)) {
2202             delete item[prop.name];
2203           }
2204
2205           /*  Format for printing if printFormat and not an object */
2206           if (printFormat && !prop.toOne && !prop.toMany) {
2207             switch(prop.attr.type) {
2208               case "Date":
2209                 preOffsetDate = item[itemAttr];
2210                 offsetDate = preOffsetDate &&
2211                   new Date(preOffsetDate.valueOf() + 60000 * preOffsetDate.getTimezoneOffset());
2212                 item[itemAttr] = XT.formatDate(offsetDate).formatdate;
2213               break;
2214               case "Cost":
2215                 item[itemAttr] = XT.formatCost(item[itemAttr]).formatcost.toString();
2216               break;
2217               case "Number":
2218                 item[itemAttr] = XT.formatNumeric(item[itemAttr], "").formatnumeric.toString();
2219               break;
2220               case "Money":
2221                 item[itemAttr] = XT.formatMoney(item[itemAttr]).formatmoney.toString();
2222               break;
2223               case "SalesPrice":
2224                 item[itemAttr] = XT.formatSalesPrice(item[itemAttr]).formatsalesprice.toString();
2225               break;
2226               case "PurchasePrice":
2227                 item[itemAttr] = XT.formatPurchPrice(item[itemAttr]).formatpurchprice.toString();
2228               break;
2229               case "ExtendedPrice":
2230                 item[itemAttr] = XT.formatExtPrice(item[itemAttr]).formatextprice.toString();
2231               break;
2232               case "Quantity":
2233                 item[itemAttr] = XT.formatQty(item[itemAttr]).formatqty.toString();
2234               break;
2235               case "QuantityPer":
2236                 item[itemAttr] = XT.formatQtyPer(item[itemAttr]).formatqtyper.toString();
2237               break;
2238               case "UnitRatioScale":
2239                 item[itemAttr] = XT.formatRatio(item[itemAttr]).formatratio.toString();
2240               break;
2241               case "Percent":
2242                 item[itemAttr] = XT.formatPrcnt(item[itemAttr]).formatprcnt.toString();
2243               break;
2244               case "WeightScale":
2245                 item[itemAttr] = XT.formatWeight(item[itemAttr]).formatweight.toString();
2246               break;
2247               default:
2248                 item[itemAttr] = (item[itemAttr] || "").toString();
2249             }
2250           }
2251
2252           /* Handle composite types */
2253           if (prop.toOne && prop.toOne.isNested && item[prop.name]) {
2254             this.sanitize(nameSpace, prop.toOne.type, item[prop.name], options);
2255           } else if (prop.toMany && prop.toMany.isNested && item[prop.name]) {
2256             for (var n = 0; n < item[prop.name].length; n++) {
2257               val = item[prop.name][n];
2258
2259               /* Remove foreign key if applicable */
2260               if (!inclKeys) { delete val[prop.toMany.inverse]; }
2261               this.sanitize(nameSpace, prop.toMany.type, val, options);
2262             }
2263           }
2264         }
2265       }
2266     },
2267
2268     /**
2269      * Returns a array of key value pairs of metric settings that correspond with an array of passed keys.
2270      *
2271      * @param {Array} array of metric names
2272      * @returns {Array}
2273      */
2274     retrieveMetrics: function (keys) {
2275       var literals = [],
2276         prop,
2277         qry,
2278         ret = {},
2279         sql = 'select metric_name as setting, metric_value as value '
2280             + 'from metric '
2281             + 'where metric_name in ({literals})';
2282
2283       for (var i = 0; i < keys.length; i++) {
2284         literals[i] = "%" + (i + 1) + "$L";
2285       }
2286
2287       sql = sql.replace(/{literals}/, literals.join(','));
2288       sql = XT.format(sql, keys)
2289
2290       if (DEBUG) {
2291         XT.debug('retrieveMetrics sql = ', sql);
2292       }
2293       qry = plv8.execute(sql);
2294
2295       /* Recast where applicable. */
2296       for (var i = 0; i < qry.length; i++) {
2297         prop = qry[i].setting;
2298         if(qry[i].value === 't') { ret[prop] = true; }
2299         else if(qry[i].value === 'f') { ret[prop] = false }
2300         else if(!isNaN(qry[i].value)) { ret[prop] = qry[i].value - 0; }
2301         else { ret[prop] = qry[i].value; }
2302       }
2303
2304       /* Make sure there is a result at all times */
2305       keys.forEach(function (key) {
2306         if (ret[key] === undefined) { ret[key] = null; }
2307       });
2308
2309       return ret;
2310     },
2311
2312     /**
2313      * Creates and returns a lock for a given table. Defaults to a time based lock of 30 seconds
2314      * unless aternate timeout option or process id (pid) is passed. If a pid is passed, the lock
2315      * is considered infinite as long as the pid is valid. If a previous lock key is passed and it is
2316      * valid, a new lock will be granted.
2317      *
2318      * @param {String | Number} Table name or oid
2319      * @param {Number} Record id
2320      * @param {Object} Options
2321      * @param {Number} [options.timeout=30]
2322      * @param {Number} [options.pid] Process id
2323      * @param {Number} [options.key] Key
2324      * @param {Boolean} [options.obtainLock=true] If false, only checks for existing lock
2325      */
2326     tryLock: function (table, id, options) {
2327       options = options ? options : {};
2328
2329       var deleteSql = "delete from xt.lock where lock_id = $1;",
2330         timeout = options.timeout || 30,
2331         expires = new Date(),
2332         i,
2333         insertSqlExp = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_expires) " +
2334                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2335         insertSqlPid = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_pid) " +
2336                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2337         lock,
2338         lockExp,
2339         oid,
2340         pcheck,
2341         pid = options.pid || null,
2342         pidSql = "select usename, procpid " +
2343                  "from pg_stat_activity " +
2344                  "where datname=current_database() " +
2345                  " and usename=$1 " +
2346                  " and procpid=$2;",
2347         query,
2348         selectSql = "select * " +
2349                     "from xt.lock " +
2350                     "where lock_table_oid = $1 " +
2351                     " and lock_record_id = $2;",
2352         username = XT.username;
2353
2354       /* If passed a table name, look up the oid. */
2355       oid = typeof table === "string" ? this.getTableOid(table) : table;
2356
2357       if (DEBUG) XT.debug("Trying lock table", [oid, id]);
2358
2359       /* See if there are existing lock(s) for this record. */
2360       if (DEBUG) {
2361         XT.debug('tryLock sql = ', selectSql);
2362         XT.debug('tryLock values = ', [oid, id]);
2363       }
2364       query = plv8.execute(selectSql, [oid, id]);
2365
2366       /* Validate result */
2367       if (query.length > 0) {
2368         while (query.length) {
2369           lock = query.shift();
2370
2371           /* See if we are confirming our own lock. */
2372           if (options.key && options.key === lock.lock_id) {
2373             /* Go on and we'll get a new lock. */
2374
2375           /* Make sure if they are pid locks users is still connected. */
2376           } else if (lock.lock_pid) {
2377             if (DEBUG) {
2378               XT.debug('tryLock sql = ', pidSql);
2379               XT.debug('tryLock values = ', [lock.lock_username, lock.lock_pid]);
2380             }
2381             pcheck = plv8.execute(pidSql, [lock.lock_username, lock.lock_pid]);
2382             if (pcheck.length) { break; } /* valid lock */
2383           } else {
2384             lockExp = new Date(lock.lock_expires);
2385             if (DEBUG) { XT.debug("Lock found", [lockExp > expires, lockExp, expires]); }
2386             if (lockExp > expires) { break; } /* valid lock */
2387           }
2388
2389           /* Delete invalid or expired lock. */
2390           if (DEBUG) {
2391             XT.debug('tryLock sql = ', deleteSql);
2392             XT.debug('tryLock values = ', [lock.lock_id]);
2393           }
2394           plv8.execute(deleteSql, [lock.lock_id]);
2395           lock = undefined;
2396         }
2397
2398         if (lock) {
2399           if (DEBUG) XT.debug("Lock found", lock.lock_username);
2400
2401           return {
2402             username: lock.lock_username,
2403             effective: lock.lock_effective
2404           }
2405         }
2406       }
2407
2408       if (options.obtainLock === false) { return; }
2409
2410       if (DEBUG) { XT.debug("Creating lock."); }
2411       if (DEBUG) { XT.debug('tryLock sql = ', insertSqlPid); }
2412
2413       if (pid) {
2414         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, pid]); }
2415         lock = plv8.execute(insertSqlPid, [oid, id, username, pid])[0];
2416       } else {
2417         expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2418         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, expires]); }
2419         lock = plv8.execute(insertSqlExp, [oid, id, username, expires])[0];
2420       }
2421
2422       if (DEBUG) { XT.debug("Lock returned is", lock.lock_id); }
2423
2424       return {
2425         username: username,
2426         effective: lock.lock_effective,
2427         key: lock.lock_id
2428       }
2429     },
2430
2431     /**
2432      * Release a lock. Pass either options with a key, or table, id and username.
2433      *
2434      * @param {Object} Options: key or table and id
2435      */
2436     releaseLock: function (options) {
2437       var oid,
2438         sqlKey = 'delete from xt.lock where lock_id = $1;',
2439         sqlUsr = 'delete from xt.lock where lock_table_oid = $1 and lock_record_id = $2 and lock_username = $3;',
2440         username = XT.username;
2441
2442       if (options.key) {
2443         if (DEBUG) {
2444           XT.debug('releaseLock sql = ', sqlKey);
2445           XT.debug('releaseLock values = ', [options.key]);
2446         }
2447         plv8.execute(sqlKey, [options.key]);
2448       } else {
2449         oid = typeof options.table === "string" ? this.getTableOid(options.table) : options.table;
2450
2451         if (DEBUG) {
2452           XT.debug('releaseLock sql = ', sqlUsr);
2453           XT.debug('releaseLock values = ', [oid, options.id, username]);
2454         }
2455         plv8.execute(sqlUsr, [oid, options.id, username]);
2456       }
2457
2458       return true;
2459     },
2460
2461     /* This deprecated function is still used by three dispatch functions. We should delete
2462     this as soon as we refactor those, and then rename buildClauseOptimized to buildClause */
2463     buildClause: function (nameSpace, type, parameters, orderBy) {
2464       parameters = parameters || [];
2465
2466       var arrayIdentifiers = [],
2467         arrayParams,
2468         charSql,
2469         childOrm,
2470         clauses = [],
2471         count = 1,
2472         identifiers = [],
2473         list = [],
2474         isArray = false,
2475         op,
2476         orClause,
2477         orderByIdentifiers = [],
2478         orderByParams = [],
2479         orm = this.fetchOrm(nameSpace, type),
2480         param,
2481         params = [],
2482         parts,
2483         pcount,
2484         prevOrm,
2485         privileges = orm.privileges,
2486         prop,
2487         ret = {};
2488
2489       ret.conditions = "";
2490       ret.parameters = [];
2491
2492       /* Handle privileges. */
2493       if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
2494       if (privileges &&
2495           (!privileges.all ||
2496             (privileges.all &&
2497               (!this.checkPrivilege(privileges.all.read) &&
2498               !this.checkPrivilege(privileges.all.update)))
2499           ) &&
2500           privileges.personal &&
2501           (this.checkPrivilege(privileges.personal.read) ||
2502             this.checkPrivilege(privileges.personal.update))
2503         ) {
2504
2505         parameters.push({
2506           attribute: privileges.personal.properties,
2507           isLower: true,
2508           isUsernamePrivFilter: true,
2509           value: XT.username
2510         });
2511       }
2512
2513       /* Handle parameters. */
2514       if (parameters.length) {
2515         for (var i = 0; i < parameters.length; i++) {
2516           orClause = [];
2517           param = parameters[i];
2518           op = param.operator || '=';
2519           switch (op) {
2520           case '=':
2521           case '>':
2522           case '<':
2523           case '>=':
2524           case '<=':
2525           case '!=':
2526             break;
2527           case 'BEGINS_WITH':
2528             op = '~^';
2529             break;
2530           case 'ENDS_WITH':
2531             op = '~?';
2532             break;
2533           case 'MATCHES':
2534             op = '~*';
2535             break;
2536           case 'ANY':
2537             op = '<@';
2538             for (var c = 0; c < param.value.length; c++) {
2539               ret.parameters.push(param.value[c]);
2540               param.value[c] = '$' + count;
2541               count++;
2542             }
2543             break;
2544           case 'NOT ANY':
2545             op = '!<@';
2546             for (var c = 0; c < param.value.length; c++) {
2547               ret.parameters.push(param.value[c]);
2548               param.value[c] = '$' + count;
2549               count++;
2550             }
2551             break;
2552           default:
2553             plv8.elog(ERROR, 'Invalid operator: ' + op);
2554           }
2555
2556           /* Handle characteristics. This is very specific to xTuple,
2557              and highly dependant on certain table structures and naming conventions,
2558              but otherwise way too much work to refactor in an abstract manner right now. */
2559           if (param.isCharacteristic) {
2560             /* Handle array. */
2561             if (op === '<@') {
2562               param.value = ' ARRAY[' + param.value.join(',') + ']';
2563             }
2564
2565             /* Booleans are stored as strings. */
2566             if (param.value === true) {
2567               param.value = 't';
2568             } else if (param.value === false) {
2569               param.value = 'f';
2570             }
2571
2572             /* Yeah, it depends on a property called 'characteristics'... */
2573             prop = XT.Orm.getProperty(orm, 'characteristics');
2574
2575             /* Build the characteristics query clause. */
2576             identifiers.push(prop.toMany.inverse);
2577             identifiers.push(orm.nameSpace.toLowerCase());
2578             identifiers.push(prop.toMany.type.decamelize());
2579             identifiers.push(param.attribute);
2580             identifiers.push(param.value);
2581
2582             charSql = 'id in (' +
2583                       '  select %' + (identifiers.length - 4) + '$I '+
2584                       '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
2585                       '    join char on (char_name = characteristic)' +
2586                       '  where 1=1 ' +
2587                       /* Note: Not using $i for these. L = literal here. These is not identifiers. */
2588                       '    and char_name = %' + (identifiers.length - 1) + '$L ' +
2589                       '    and value ' + op + ' %' + (identifiers.length) + '$L ' +
2590                       ')';
2591
2592             clauses.push(charSql);
2593
2594           /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
2595           } else if (op === '<@' || op === '!<@') {
2596             /* Handle paths if applicable. */
2597             if (param.attribute.indexOf('.') > -1) {
2598               parts = param.attribute.split('.');
2599               childOrm = this.fetchOrm(nameSpace, type);
2600               params.push("");
2601               pcount = params.length - 1;
2602
2603               for (var n = 0; n < parts.length; n++) {
2604                 /* Validate attribute. */
2605                 prop = XT.Orm.getProperty(childOrm, parts[n]);
2606                 if (!prop) {
2607                   plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
2608                 }
2609
2610                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
2611                 identifiers.push(parts[n]);
2612                 params[pcount] += "%" + identifiers.length + "$I";
2613                 if (n < parts.length - 1) {
2614                   params[pcount] = "(" + params[pcount] + ").";
2615                   childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2616                 } else {
2617                   params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
2618                 }
2619               }
2620             } else {
2621               identifiers.push(param.attribute);
2622               params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
2623               pcount = params.length - 1;
2624             }
2625             clauses.push(params[pcount]);
2626
2627           /* Everything else handle another. */
2628           } else {
2629             if (XT.typeOf(param.attribute) !== 'array') {
2630               param.attribute = [param.attribute];
2631             }
2632
2633             for (var c = 0; c < param.attribute.length; c++) {
2634               /* Handle paths if applicable. */
2635               if (param.attribute[c].indexOf('.') > -1) {
2636                 parts = param.attribute[c].split('.');
2637                 childOrm = this.fetchOrm(nameSpace, type);
2638                 params.push("");
2639                 pcount = params.length - 1;
2640                 isArray = false;
2641
2642                 /* Check if last part is an Array. */
2643                 for (var m = 0; m < parts.length; m++) {
2644                   /* Validate attribute. */
2645                   prop = XT.Orm.getProperty(childOrm, parts[m]);
2646                   if (!prop) {
2647                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
2648                   }
2649
2650                   if (m < parts.length - 1) {
2651                     childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2652                   } else if (prop.attr && prop.attr.type === 'Array') {
2653                     /* The last property in the path is an array. */
2654                     isArray = true;
2655                     params[pcount] = '$' + count;
2656                   }
2657                 }
2658
2659                 /* Reset the childOrm to parent. */
2660                 childOrm = this.fetchOrm(nameSpace, type);
2661
2662                 for (var n = 0; n < parts.length; n++) {
2663                   /* Validate attribute. */
2664                   prop = XT.Orm.getProperty(childOrm, parts[n]);
2665                   if (!prop) {
2666                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
2667                   }
2668
2669                   /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
2670                   if (param.isUsernamePrivFilter && isArray) {
2671                     identifiers.push(parts[n]);
2672                     arrayIdentifiers.push(identifiers.length);
2673
2674                     if (n < parts.length - 1) {
2675                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2676                     }
2677                   } else {
2678                     /* Build path. e.g. ((%1$I).%2$I).%3$I */
2679                     identifiers.push(parts[n]);
2680                     params[pcount] += "%" + identifiers.length + "$I";
2681
2682                     if (n < parts.length - 1) {
2683                       params[pcount] = "(" + params[pcount] + ").";
2684                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2685                     } else if (param.isLower) {
2686                       params[pcount] = "lower(" + params[pcount] + ")";
2687                     }
2688                   }
2689                 }
2690               } else {
2691                 /* Validate attribute. */
2692                 prop = XT.Orm.getProperty(orm, param.attribute[c]);
2693                 if (!prop) {
2694                   plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
2695                 }
2696
2697                 identifiers.push(param.attribute[c]);
2698
2699                 /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
2700                 if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
2701                   (prop.attr && prop.attr.type === 'Array'))) {
2702
2703                   params.push('$' + count);
2704                   pcount = params.length - 1;
2705                   arrayIdentifiers.push(identifiers.length);
2706                 } else {
2707                   params.push("%" + identifiers.length + "$I");
2708                   pcount = params.length - 1;
2709                 }
2710               }
2711
2712               /* Add persional privs array search. */
2713               if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
2714                 || (prop.attr && prop.attr.type === 'Array') || isArray)) {
2715
2716                 /* e.g. 'admin' = ANY (usernames_array) */
2717                 arrayParams = "";
2718                 params[pcount] += ' ' + op + ' ANY (';
2719
2720                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
2721                 for (var f =0; f < arrayIdentifiers.length; f++) {
2722                   arrayParams += '%' + arrayIdentifiers[f] + '$I';
2723                   if (f < arrayIdentifiers.length - 1) {
2724                     arrayParams = "(" + arrayParams + ").";
2725                   }
2726                 }
2727                 params[pcount] += arrayParams + ')';
2728
2729               /* Add optional is null clause. */
2730               } else if (parameters[i].includeNull) {
2731                 /* e.g. %1$I = $1 or %1$I is null */
2732                 params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
2733               } else {
2734                 /* e.g. %1$I = $1 */
2735                 params[pcount] += " " + op + ' $' + count;
2736               }
2737
2738               orClause.push(params[pcount]);
2739             }
2740
2741             /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
2742             clauses.push('(' + orClause.join(' or ') + ')');
2743             count++;
2744             ret.parameters.push(param.value);
2745           }
2746         }
2747       }
2748
2749       ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
2750
2751       /* Massage ordeBy with quoted identifiers. */
2752       if (orderBy) {
2753         for (var i = 0; i < orderBy.length; i++) {
2754           /* Handle path case. */
2755           if (orderBy[i].attribute.indexOf('.') > -1) {
2756             parts = orderBy[i].attribute.split('.');
2757             prevOrm = orm;
2758             orderByParams.push("");
2759             pcount = orderByParams.length - 1;
2760
2761             for (var n = 0; n < parts.length; n++) {
2762               prop = XT.Orm.getProperty(orm, parts[n]);
2763               if (!prop) {
2764                 plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
2765               }
2766               orderByIdentifiers.push(parts[n]);
2767               orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
2768
2769               if (n < parts.length - 1) {
2770                 orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
2771                 orm = this.fetchOrm(nameSpace, prop.toOne.type);
2772               }
2773             }
2774             orm = prevOrm;
2775           /* Normal case. */
2776           } else {
2777             prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
2778             if (!prop) {
2779               plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
2780             }
2781             orderByIdentifiers.push(orderBy[i].attribute);
2782             orderByParams.push("%" + orderByIdentifiers.length + "$I");
2783             pcount = orderByParams.length - 1;
2784           }
2785
2786           if (orderBy[i].isEmpty) {
2787             orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
2788           }
2789           if (orderBy[i].descending) {
2790             orderByParams[pcount] += " desc";
2791           }
2792
2793           list.push(orderByParams[pcount])
2794         }
2795       }
2796
2797       ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
2798
2799       return ret;
2800     },
2801     /**
2802      * Renew a lock. Defaults to rewing the lock for 30 seconds.
2803      *
2804      * @param {Number} Key
2805      * @params {Object} Options: timeout
2806      * @returns {Date} New expiration or false.
2807      */
2808     renewLock: function (key, options) {
2809       var expires = new Date(),
2810         query,
2811         selectSql = "select * from xt.lock where lock_id = $1;",
2812         timeout = options && options.timeout ? options.timeout : 30,
2813         updateSql = "update xt.lock set lock_expires = $1 where lock_id = $2;";
2814
2815       if (typeof key !== "number") { return false; }
2816       expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2817
2818       if (DEBUG) {
2819         XT.debug('renewLock sql = ', selectSql);
2820         XT.debug('renewLock values = ', [key]);
2821       }
2822       query = plv8.execute(selectSql, [key]);
2823
2824       if (query.length) {
2825         if (DEBUG) {
2826           XT.debug('renewLock sql = ', updateSql);
2827           XT.debug('renewLock values = ', [expires, key]);
2828         }
2829         plv8.execute(updateSql, [expires, key]);
2830
2831         return true;
2832       }
2833
2834       return false;
2835     }
2836   }
2837
2838 }());
2839
2840 $$ );