Also check for undefined orderBy
[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 (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 sql = ', sql);
1836           XT.debug('getVersion 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       /* Because we query views of views, you can get inconsistent results */
1911       /* when doing limit and offest queries without an order by. Add a default. */
1912       if (limit && offset && (!orderBy || !orderBy.length) && !clause.orderByColumns) {
1913         /* We only want this on sql1, not sql2's clause.orderBy. */
1914         clause.orderByColumns = XT.format('order by t1.%1$I', [pkeyColumn]);
1915       }
1916
1917       /* Query the model. */
1918       sql1 = XT.format(sql1, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
1919       sql1 = sql1.replace('{joins}', clause.joins)
1920                  .replace('{conditions}', clause.conditions)
1921                  .replace(/{groupBy}/g, clause.groupByColumns)
1922                  .replace(/{orderBy}/g, clause.orderByColumns)
1923                  .replace('{limit}', limit)
1924                  .replace('{offset}', offset);
1925
1926       if (DEBUG) {
1927         XT.debug('fetch sql1 = ', sql1);
1928         XT.debug('fetch values = ', clause.parameters);
1929       }
1930
1931       /* First query for matching ids, then get entire result set. */
1932       /* This improves performance over a direct query on the view due */
1933       /* to the way sorting is handled by the query optimizer */
1934       qry = plv8.execute(sql1, clause.parameters) || [];
1935       if (!qry.length) { return [] };
1936       qry.forEach(function (row) {
1937         ids.push(row.id);
1938         idParams.push("$" + counter);
1939         counter++;
1940       });
1941
1942       if (orm.lockable) {
1943         sql_etags = "select ver_etag as etag, ver_record_id as id " +
1944                     "from xt.ver " +
1945                     "where ver_table_oid = ( " +
1946                       "select pg_class.oid::integer as oid " +
1947                       "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
1948                       /* Note: using $L for quoted literal e.g. 'contact', not an identifier. */
1949                       "where nspname = %1$L and relname = %2$L " +
1950                     ") " +
1951                     "and ver_record_id in ({ids})";
1952         sql_etags = XT.format(sql_etags, [tableNamespace, table]);
1953         sql_etags = sql_etags.replace('{ids}', idParams.join());
1954
1955         if (DEBUG) {
1956           XT.debug('fetch sql_etags = ', sql_etags);
1957           XT.debug('fetch etags_values = ', JSON.stringify(ids));
1958         }
1959         etags = plv8.execute(sql_etags, ids) || {};
1960         ret.etags = {};
1961       }
1962
1963       sql2 = XT.format(sql2, [nameSpace.decamelize(), type.decamelize(), pkey]);
1964       sql2 = sql2.replace(/{orderBy}/g, clause.orderBy)
1965                  .replace('{ids}', idParams.join());
1966
1967       if (DEBUG) {
1968         XT.debug('fetch sql2 = ', sql2);
1969         XT.debug('fetch values = ', JSON.stringify(ids));
1970       }
1971       ret.data = plv8.execute(sql2, ids) || [];
1972
1973       for (var i = 0; i < ret.data.length; i++) {
1974         ret.data[i] = this.decrypt(nameSpace, type, ret.data[i], encryptionKey);
1975
1976         if (etags) {
1977           /* Add etags to result in pkey->etag format. */
1978           for (var j = 0; j < etags.length; j++) {
1979             if (etags[j].id === ret.data[i][pkey]) {
1980               ret.etags[ret.data[i][nkey]] = etags[j].etag;
1981             }
1982           }
1983         }
1984       }
1985
1986       this.sanitize(nameSpace, type, ret.data, options);
1987
1988       return ret;
1989     },
1990
1991     /**
1992     Fetch a metric value.
1993
1994     @param {String} Metric name
1995     @param {String} Return type 'text', 'boolean' or 'number' (default 'text')
1996     */
1997     fetchMetric: function (name, type) {
1998       var fn = 'fetchmetrictext';
1999       if (type === 'boolean') {
2000         fn = 'fetchmetricbool';
2001       } else if (type === 'number') {
2002         fn = 'fetchmetricvalue';
2003       }
2004       return plv8.execute("select " + fn + "($1) as resp", [name])[0].resp;
2005     },
2006
2007     /**
2008      * Retreives a record from the database. If the user does not have appropriate privileges an
2009      * error will be thrown unless the `silentError` option is passed.
2010      *
2011      * If `context` is passed as an option then a record will only be returned if it exists in the context (parent)
2012      * record which itself must be accessible by the effective user.
2013      *
2014      * @param {Object} options
2015      * @param {String} [options.nameSpace] Namespace. Required.
2016      * @param {String} [options.type] Type. Required.
2017      * @param {Number} [options.id] Record id. Required.
2018      * @param {Boolean} [options.superUser=false] If true ignore privilege checking.
2019      * @param {String} [options.encryptionKey] Encryption key
2020      * @param {Boolean} [options.silentError=false] Silence errors
2021      * @param {Object} [options.context] Context
2022      * @param {String} [options.context.nameSpace] Context namespace.
2023      * @param {String} [options.context.type] The type of context object.
2024      * @param {String} [options.context.value] The value of the context's primary key.
2025      * @param {String} [options.context.relation] The name of the attribute on the type to which this record is related.
2026      * @returns Object
2027      */
2028     retrieveRecord: function (options) {
2029       options = options ? options : {};
2030       options.obtainLock = false;
2031
2032       var id = options.id,
2033         nameSpace = options.nameSpace,
2034         type = options.type,
2035         map = this.fetchOrm(nameSpace, type),
2036         context = options.context,
2037         encryptionKey = options.encryptionKey,
2038         join = "",
2039         lockTable = map.lockTable || map.table,
2040         nkey = XT.Orm.naturalKey(map),
2041         params = {},
2042         pkey = XT.Orm.primaryKey(map),
2043         ret = {
2044           nameSpace: nameSpace,
2045           type: type,
2046           id: id
2047         },
2048         sql;
2049
2050       if (!pkey) {
2051         throw new Error('No key found for {nameSpace}.{type}'
2052                         .replace("{nameSpace}", nameSpace)
2053                         .replace("{type}", type));
2054       }
2055
2056       /* If this object uses a natural key, go get the primary key id. */
2057       if (nkey) {
2058         id = this.getId(map, id);
2059         if (!id) {
2060           return false;
2061         }
2062       }
2063
2064       /* Context means search for this record inside another. */
2065       if (context) {
2066         context.nameSpace = context.nameSpace || context.recordType.beforeDot();
2067         context.type = context.type || context.recordType.afterDot()
2068         context.map = this.fetchOrm(context.nameSpace, context.type);
2069         context.prop = XT.Orm.getProperty(context.map, context.relation);
2070         context.pertinentExtension = XT.Orm.getProperty(context.map, context.relation, true);
2071         context.underlyingTable = context.pertinentExtension.table,
2072         context.underlyingNameSpace = this.getNamespaceFromNamespacedTable(context.underlyingTable);
2073         context.underlyingType = this.getTableFromNamespacedTable(context.underlyingTable);
2074         context.fkey = context.prop.toMany.inverse;
2075         context.fkeyColumn = context.prop.toMany.column;
2076         context.pkey = XT.Orm.naturalKey(context.map) || XT.Orm.primaryKey(context.map);
2077         params.attribute = context.pkey;
2078         params.value = context.value;
2079
2080         join = 'join %1$I.%2$I on (%1$I.%2$I.%3$I = %4$I.%5$I)';
2081         join = XT.format(join, [
2082             context.underlyingNameSpace,
2083             context.underlyingType,
2084             context.fkeyColumn,
2085             type.decamelize(),
2086             context.fkey
2087           ]);
2088       }
2089
2090       /* Validate - don't bother running the query if the user has no privileges. */
2091       if(!options.superUser && !context && !this.checkPrivileges(nameSpace, type)) {
2092         if (options.silentError) {
2093           return false;
2094         } else {
2095           throw new handleError("Unauthorized", 401);
2096         }
2097       }
2098
2099       ret.etag = this.getVersion(map, id);
2100
2101       /* Obtain lock if required. */
2102       if (map.lockable) {
2103         ret.lock = this.tryLock(lockTable, id, options);
2104       }
2105
2106       /* Data sql. */
2107       sql = 'select %1$I.* from %2$I.%1$I {join} where %1$I.%3$I = $1;';
2108       sql = sql.replace(/{join}/, join);
2109       sql = XT.format(sql, [type.decamelize(), nameSpace.decamelize(), pkey]);
2110
2111       /* Query the map. */
2112       if (DEBUG) {
2113         XT.debug('retrieveRecord sql = ', sql);
2114         XT.debug('retrieveRecord values = ', [id]);
2115       }
2116       ret.data = plv8.execute(sql, [id])[0] || {};
2117
2118       if (!context) {
2119         /* Check privileges again, this time against record specific criteria where applicable. */
2120         if(!options.superUser && !this.checkPrivileges(nameSpace, type, ret.data)) {
2121           if (options.silentError) {
2122             return false;
2123           } else {
2124             throw new handleError("Unauthorized", 401);
2125           }
2126         }
2127         /* Decrypt result where applicable. */
2128         ret.data = this.decrypt(nameSpace, type, ret.data, encryptionKey);
2129       }
2130
2131       this.sanitize(nameSpace, type, ret.data, options);
2132
2133       /* Return the results. */
2134       return ret || {};
2135     },
2136
2137     /**
2138      *  Remove unprivileged attributes, primary and foreign keys from the data.
2139      *  Only removes the primary key if a natural key has been specified in the ORM.
2140      *  Also format for printing using XT.format functions if printFormat=true'
2141      *
2142      * @param {String} Namespace
2143      * @param {String} Type
2144      * @param {Object|Array} Data
2145      * @param {Object} Options
2146      * @param {Boolean} [options.includeKeys=false] Do not remove primary and foreign keys.
2147      * @param {Boolean} [options.superUser=false] Do not remove unprivileged attributes.
2148      * @param {Boolean} [options.printFormat=true] Format for printing.
2149      */
2150     sanitize: function (nameSpace, type, data, options) {
2151       options = options || {};
2152       if (options.includeKeys && options.superUser) { return; }
2153       if (XT.typeOf(data) !== "array") { data = [data]; }
2154       var orm = this.fetchOrm(nameSpace, type),
2155         pkey = XT.Orm.primaryKey(orm),
2156         nkey = XT.Orm.naturalKey(orm),
2157         props = orm.properties,
2158         attrPriv = orm.privileges && orm.privileges.attribute ?
2159           orm.privileges.attribute : false,
2160         inclKeys = options.includeKeys,
2161         superUser = options.superUser,
2162         printFormat = options.printFormat,
2163         c,
2164         i,
2165         item,
2166         n,
2167         prop,
2168         itemAttr,
2169         filteredProps,
2170         val,
2171         preOffsetDate,
2172         offsetDate,
2173         check = function (p) {
2174           return p.name === itemAttr;
2175         };
2176
2177       for (var c = 0; c < data.length; c++) {
2178         item = data[c];
2179
2180         /* Remove primary key if applicable */
2181         if (!inclKeys && nkey && nkey !== pkey) { delete item[pkey]; }
2182
2183         for (itemAttr in item) {
2184           if (!item.hasOwnProperty(itemAttr)) {
2185             continue;
2186           }
2187           filteredProps = orm.properties.filter(check);
2188
2189           if (filteredProps.length === 0 && orm.extensions.length > 0) {
2190             /* Try to get the orm prop from an extension if it's not in the core*/
2191             orm.extensions.forEach(function (ext) {
2192               if (filteredProps.length === 0) {
2193                 filteredProps = ext.properties.filter(check);
2194               }
2195             });
2196           }
2197
2198           /* Remove attributes not found in the ORM */
2199           if (filteredProps.length === 0) {
2200             delete item[itemAttr];
2201           } else {
2202             prop = filteredProps[0];
2203           }
2204
2205           /* Remove unprivileged attribute if applicable */
2206           if (!superUser && attrPriv && attrPriv[prop.name] &&
2207             (attrPriv[prop.name].view !== undefined) &&
2208             !this.checkPrivilege(attrPriv[prop.name].view)) {
2209             delete item[prop.name];
2210           }
2211
2212           /*  Format for printing if printFormat and not an object */
2213           if (printFormat && !prop.toOne && !prop.toMany) {
2214             switch(prop.attr.type) {
2215               case "Date":
2216                 preOffsetDate = item[itemAttr];
2217                 offsetDate = preOffsetDate &&
2218                   new Date(preOffsetDate.valueOf() + 60000 * preOffsetDate.getTimezoneOffset());
2219                 item[itemAttr] = XT.formatDate(offsetDate).formatdate;
2220               break;
2221               case "Cost":
2222                 item[itemAttr] = XT.formatCost(item[itemAttr]).formatcost.toString();
2223               break;
2224               case "Number":
2225                 item[itemAttr] = XT.formatNumeric(item[itemAttr], "").formatnumeric.toString();
2226               break;
2227               case "Money":
2228                 item[itemAttr] = XT.formatMoney(item[itemAttr]).formatmoney.toString();
2229               break;
2230               case "SalesPrice":
2231                 item[itemAttr] = XT.formatSalesPrice(item[itemAttr]).formatsalesprice.toString();
2232               break;
2233               case "PurchasePrice":
2234                 item[itemAttr] = XT.formatPurchPrice(item[itemAttr]).formatpurchprice.toString();
2235               break;
2236               case "ExtendedPrice":
2237                 item[itemAttr] = XT.formatExtPrice(item[itemAttr]).formatextprice.toString();
2238               break;
2239               case "Quantity":
2240                 item[itemAttr] = XT.formatQty(item[itemAttr]).formatqty.toString();
2241               break;
2242               case "QuantityPer":
2243                 item[itemAttr] = XT.formatQtyPer(item[itemAttr]).formatqtyper.toString();
2244               break;
2245               case "UnitRatioScale":
2246                 item[itemAttr] = XT.formatRatio(item[itemAttr]).formatratio.toString();
2247               break;
2248               case "Percent":
2249                 item[itemAttr] = XT.formatPrcnt(item[itemAttr]).formatprcnt.toString();
2250               break;
2251               case "WeightScale":
2252                 item[itemAttr] = XT.formatWeight(item[itemAttr]).formatweight.toString();
2253               break;
2254               default:
2255                 item[itemAttr] = (item[itemAttr] || "").toString();
2256             }
2257           }
2258
2259           /* Handle composite types */
2260           if (prop.toOne && prop.toOne.isNested && item[prop.name]) {
2261             this.sanitize(nameSpace, prop.toOne.type, item[prop.name], options);
2262           } else if (prop.toMany && prop.toMany.isNested && item[prop.name]) {
2263             for (var n = 0; n < item[prop.name].length; n++) {
2264               val = item[prop.name][n];
2265
2266               /* Remove foreign key if applicable */
2267               if (!inclKeys) { delete val[prop.toMany.inverse]; }
2268               this.sanitize(nameSpace, prop.toMany.type, val, options);
2269             }
2270           }
2271         }
2272       }
2273     },
2274
2275     /**
2276      * Returns a array of key value pairs of metric settings that correspond with an array of passed keys.
2277      *
2278      * @param {Array} array of metric names
2279      * @returns {Array}
2280      */
2281     retrieveMetrics: function (keys) {
2282       var literals = [],
2283         prop,
2284         qry,
2285         ret = {},
2286         sql = 'select metric_name as setting, metric_value as value '
2287             + 'from metric '
2288             + 'where metric_name in ({literals})';
2289
2290       for (var i = 0; i < keys.length; i++) {
2291         literals[i] = "%" + (i + 1) + "$L";
2292       }
2293
2294       sql = sql.replace(/{literals}/, literals.join(','));
2295       sql = XT.format(sql, keys)
2296
2297       if (DEBUG) {
2298         XT.debug('retrieveMetrics sql = ', sql);
2299       }
2300       qry = plv8.execute(sql);
2301
2302       /* Recast where applicable. */
2303       for (var i = 0; i < qry.length; i++) {
2304         prop = qry[i].setting;
2305         if(qry[i].value === 't') { ret[prop] = true; }
2306         else if(qry[i].value === 'f') { ret[prop] = false }
2307         else if(!isNaN(qry[i].value)) { ret[prop] = qry[i].value - 0; }
2308         else { ret[prop] = qry[i].value; }
2309       }
2310
2311       /* Make sure there is a result at all times */
2312       keys.forEach(function (key) {
2313         if (ret[key] === undefined) { ret[key] = null; }
2314       });
2315
2316       return ret;
2317     },
2318
2319     /**
2320      * Creates and returns a lock for a given table. Defaults to a time based lock of 30 seconds
2321      * unless aternate timeout option or process id (pid) is passed. If a pid is passed, the lock
2322      * is considered infinite as long as the pid is valid. If a previous lock key is passed and it is
2323      * valid, a new lock will be granted.
2324      *
2325      * @param {String | Number} Table name or oid
2326      * @param {Number} Record id
2327      * @param {Object} Options
2328      * @param {Number} [options.timeout=30]
2329      * @param {Number} [options.pid] Process id
2330      * @param {Number} [options.key] Key
2331      * @param {Boolean} [options.obtainLock=true] If false, only checks for existing lock
2332      */
2333     tryLock: function (table, id, options) {
2334       options = options ? options : {};
2335
2336       var deleteSql = "delete from xt.lock where lock_id = $1;",
2337         timeout = options.timeout || 30,
2338         expires = new Date(),
2339         i,
2340         insertSqlExp = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_expires) " +
2341                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2342         insertSqlPid = "insert into xt.lock (lock_table_oid, lock_record_id, lock_username, lock_pid) " +
2343                        "values ($1, $2, $3, $4) returning lock_id, lock_effective;",
2344         lock,
2345         lockExp,
2346         oid,
2347         pcheck,
2348         pid = options.pid || null,
2349         pidSql = "select usename, procpid " +
2350                  "from pg_stat_activity " +
2351                  "where datname=current_database() " +
2352                  " and usename=$1 " +
2353                  " and procpid=$2;",
2354         query,
2355         selectSql = "select * " +
2356                     "from xt.lock " +
2357                     "where lock_table_oid = $1 " +
2358                     " and lock_record_id = $2;",
2359         username = XT.username;
2360
2361       /* If passed a table name, look up the oid. */
2362       oid = typeof table === "string" ? this.getTableOid(table) : table;
2363
2364       if (DEBUG) XT.debug("Trying lock table", [oid, id]);
2365
2366       /* See if there are existing lock(s) for this record. */
2367       if (DEBUG) {
2368         XT.debug('tryLock sql = ', selectSql);
2369         XT.debug('tryLock values = ', [oid, id]);
2370       }
2371       query = plv8.execute(selectSql, [oid, id]);
2372
2373       /* Validate result */
2374       if (query.length > 0) {
2375         while (query.length) {
2376           lock = query.shift();
2377
2378           /* See if we are confirming our own lock. */
2379           if (options.key && options.key === lock.lock_id) {
2380             /* Go on and we'll get a new lock. */
2381
2382           /* Make sure if they are pid locks users is still connected. */
2383           } else if (lock.lock_pid) {
2384             if (DEBUG) {
2385               XT.debug('tryLock sql = ', pidSql);
2386               XT.debug('tryLock values = ', [lock.lock_username, lock.lock_pid]);
2387             }
2388             pcheck = plv8.execute(pidSql, [lock.lock_username, lock.lock_pid]);
2389             if (pcheck.length) { break; } /* valid lock */
2390           } else {
2391             lockExp = new Date(lock.lock_expires);
2392             if (DEBUG) { XT.debug("Lock found", [lockExp > expires, lockExp, expires]); }
2393             if (lockExp > expires) { break; } /* valid lock */
2394           }
2395
2396           /* Delete invalid or expired lock. */
2397           if (DEBUG) {
2398             XT.debug('tryLock sql = ', deleteSql);
2399             XT.debug('tryLock values = ', [lock.lock_id]);
2400           }
2401           plv8.execute(deleteSql, [lock.lock_id]);
2402           lock = undefined;
2403         }
2404
2405         if (lock) {
2406           if (DEBUG) XT.debug("Lock found", lock.lock_username);
2407
2408           return {
2409             username: lock.lock_username,
2410             effective: lock.lock_effective
2411           }
2412         }
2413       }
2414
2415       if (options.obtainLock === false) { return; }
2416
2417       if (DEBUG) { XT.debug("Creating lock."); }
2418       if (DEBUG) { XT.debug('tryLock sql = ', insertSqlPid); }
2419
2420       if (pid) {
2421         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, pid]); }
2422         lock = plv8.execute(insertSqlPid, [oid, id, username, pid])[0];
2423       } else {
2424         expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2425         if (DEBUG) { XT.debug('tryLock values = ', [oid, id, username, expires]); }
2426         lock = plv8.execute(insertSqlExp, [oid, id, username, expires])[0];
2427       }
2428
2429       if (DEBUG) { XT.debug("Lock returned is", lock.lock_id); }
2430
2431       return {
2432         username: username,
2433         effective: lock.lock_effective,
2434         key: lock.lock_id
2435       }
2436     },
2437
2438     /**
2439      * Release a lock. Pass either options with a key, or table, id and username.
2440      *
2441      * @param {Object} Options: key or table and id
2442      */
2443     releaseLock: function (options) {
2444       var oid,
2445         sqlKey = 'delete from xt.lock where lock_id = $1;',
2446         sqlUsr = 'delete from xt.lock where lock_table_oid = $1 and lock_record_id = $2 and lock_username = $3;',
2447         username = XT.username;
2448
2449       if (options.key) {
2450         if (DEBUG) {
2451           XT.debug('releaseLock sql = ', sqlKey);
2452           XT.debug('releaseLock values = ', [options.key]);
2453         }
2454         plv8.execute(sqlKey, [options.key]);
2455       } else {
2456         oid = typeof options.table === "string" ? this.getTableOid(options.table) : options.table;
2457
2458         if (DEBUG) {
2459           XT.debug('releaseLock sql = ', sqlUsr);
2460           XT.debug('releaseLock values = ', [oid, options.id, username]);
2461         }
2462         plv8.execute(sqlUsr, [oid, options.id, username]);
2463       }
2464
2465       return true;
2466     },
2467
2468     /* This deprecated function is still used by three dispatch functions. We should delete
2469     this as soon as we refactor those, and then rename buildClauseOptimized to buildClause */
2470     buildClause: function (nameSpace, type, parameters, orderBy) {
2471       parameters = parameters || [];
2472
2473       var arrayIdentifiers = [],
2474         arrayParams,
2475         charSql,
2476         childOrm,
2477         clauses = [],
2478         count = 1,
2479         identifiers = [],
2480         list = [],
2481         isArray = false,
2482         op,
2483         orClause,
2484         orderByIdentifiers = [],
2485         orderByParams = [],
2486         orm = this.fetchOrm(nameSpace, type),
2487         param,
2488         params = [],
2489         parts,
2490         pcount,
2491         prevOrm,
2492         privileges = orm.privileges,
2493         prop,
2494         ret = {};
2495
2496       ret.conditions = "";
2497       ret.parameters = [];
2498
2499       /* Handle privileges. */
2500       if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
2501       if (privileges &&
2502           (!privileges.all ||
2503             (privileges.all &&
2504               (!this.checkPrivilege(privileges.all.read) &&
2505               !this.checkPrivilege(privileges.all.update)))
2506           ) &&
2507           privileges.personal &&
2508           (this.checkPrivilege(privileges.personal.read) ||
2509             this.checkPrivilege(privileges.personal.update))
2510         ) {
2511
2512         parameters.push({
2513           attribute: privileges.personal.properties,
2514           isLower: true,
2515           isUsernamePrivFilter: true,
2516           value: XT.username
2517         });
2518       }
2519
2520       /* Handle parameters. */
2521       if (parameters.length) {
2522         for (var i = 0; i < parameters.length; i++) {
2523           orClause = [];
2524           param = parameters[i];
2525           op = param.operator || '=';
2526           switch (op) {
2527           case '=':
2528           case '>':
2529           case '<':
2530           case '>=':
2531           case '<=':
2532           case '!=':
2533             break;
2534           case 'BEGINS_WITH':
2535             op = '~^';
2536             break;
2537           case 'ENDS_WITH':
2538             op = '~?';
2539             break;
2540           case 'MATCHES':
2541             op = '~*';
2542             break;
2543           case 'ANY':
2544             op = '<@';
2545             for (var c = 0; c < param.value.length; c++) {
2546               ret.parameters.push(param.value[c]);
2547               param.value[c] = '$' + count;
2548               count++;
2549             }
2550             break;
2551           case 'NOT ANY':
2552             op = '!<@';
2553             for (var c = 0; c < param.value.length; c++) {
2554               ret.parameters.push(param.value[c]);
2555               param.value[c] = '$' + count;
2556               count++;
2557             }
2558             break;
2559           default:
2560             plv8.elog(ERROR, 'Invalid operator: ' + op);
2561           }
2562
2563           /* Handle characteristics. This is very specific to xTuple,
2564              and highly dependant on certain table structures and naming conventions,
2565              but otherwise way too much work to refactor in an abstract manner right now. */
2566           if (param.isCharacteristic) {
2567             /* Handle array. */
2568             if (op === '<@') {
2569               param.value = ' ARRAY[' + param.value.join(',') + ']';
2570             }
2571
2572             /* Booleans are stored as strings. */
2573             if (param.value === true) {
2574               param.value = 't';
2575             } else if (param.value === false) {
2576               param.value = 'f';
2577             }
2578
2579             /* Yeah, it depends on a property called 'characteristics'... */
2580             prop = XT.Orm.getProperty(orm, 'characteristics');
2581
2582             /* Build the characteristics query clause. */
2583             identifiers.push(prop.toMany.inverse);
2584             identifiers.push(orm.nameSpace.toLowerCase());
2585             identifiers.push(prop.toMany.type.decamelize());
2586             identifiers.push(param.attribute);
2587             identifiers.push(param.value);
2588
2589             charSql = 'id in (' +
2590                       '  select %' + (identifiers.length - 4) + '$I '+
2591                       '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
2592                       '    join char on (char_name = characteristic)' +
2593                       '  where 1=1 ' +
2594                       /* Note: Not using $i for these. L = literal here. These is not identifiers. */
2595                       '    and char_name = %' + (identifiers.length - 1) + '$L ' +
2596                       '    and value ' + op + ' %' + (identifiers.length) + '$L ' +
2597                       ')';
2598
2599             clauses.push(charSql);
2600
2601           /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
2602           } else if (op === '<@' || op === '!<@') {
2603             /* Handle paths if applicable. */
2604             if (param.attribute.indexOf('.') > -1) {
2605               parts = param.attribute.split('.');
2606               childOrm = this.fetchOrm(nameSpace, type);
2607               params.push("");
2608               pcount = params.length - 1;
2609
2610               for (var n = 0; n < parts.length; n++) {
2611                 /* Validate attribute. */
2612                 prop = XT.Orm.getProperty(childOrm, parts[n]);
2613                 if (!prop) {
2614                   plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
2615                 }
2616
2617                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
2618                 identifiers.push(parts[n]);
2619                 params[pcount] += "%" + identifiers.length + "$I";
2620                 if (n < parts.length - 1) {
2621                   params[pcount] = "(" + params[pcount] + ").";
2622                   childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2623                 } else {
2624                   params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
2625                 }
2626               }
2627             } else {
2628               identifiers.push(param.attribute);
2629               params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
2630               pcount = params.length - 1;
2631             }
2632             clauses.push(params[pcount]);
2633
2634           /* Everything else handle another. */
2635           } else {
2636             if (XT.typeOf(param.attribute) !== 'array') {
2637               param.attribute = [param.attribute];
2638             }
2639
2640             for (var c = 0; c < param.attribute.length; c++) {
2641               /* Handle paths if applicable. */
2642               if (param.attribute[c].indexOf('.') > -1) {
2643                 parts = param.attribute[c].split('.');
2644                 childOrm = this.fetchOrm(nameSpace, type);
2645                 params.push("");
2646                 pcount = params.length - 1;
2647                 isArray = false;
2648
2649                 /* Check if last part is an Array. */
2650                 for (var m = 0; m < parts.length; m++) {
2651                   /* Validate attribute. */
2652                   prop = XT.Orm.getProperty(childOrm, parts[m]);
2653                   if (!prop) {
2654                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
2655                   }
2656
2657                   if (m < parts.length - 1) {
2658                     childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2659                   } else if (prop.attr && prop.attr.type === 'Array') {
2660                     /* The last property in the path is an array. */
2661                     isArray = true;
2662                     params[pcount] = '$' + count;
2663                   }
2664                 }
2665
2666                 /* Reset the childOrm to parent. */
2667                 childOrm = this.fetchOrm(nameSpace, type);
2668
2669                 for (var n = 0; n < parts.length; n++) {
2670                   /* Validate attribute. */
2671                   prop = XT.Orm.getProperty(childOrm, parts[n]);
2672                   if (!prop) {
2673                     plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
2674                   }
2675
2676                   /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
2677                   if (param.isUsernamePrivFilter && isArray) {
2678                     identifiers.push(parts[n]);
2679                     arrayIdentifiers.push(identifiers.length);
2680
2681                     if (n < parts.length - 1) {
2682                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2683                     }
2684                   } else {
2685                     /* Build path. e.g. ((%1$I).%2$I).%3$I */
2686                     identifiers.push(parts[n]);
2687                     params[pcount] += "%" + identifiers.length + "$I";
2688
2689                     if (n < parts.length - 1) {
2690                       params[pcount] = "(" + params[pcount] + ").";
2691                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
2692                     } else if (param.isLower) {
2693                       params[pcount] = "lower(" + params[pcount] + ")";
2694                     }
2695                   }
2696                 }
2697               } else {
2698                 /* Validate attribute. */
2699                 prop = XT.Orm.getProperty(orm, param.attribute[c]);
2700                 if (!prop) {
2701                   plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
2702                 }
2703
2704                 identifiers.push(param.attribute[c]);
2705
2706                 /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
2707                 if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
2708                   (prop.attr && prop.attr.type === 'Array'))) {
2709
2710                   params.push('$' + count);
2711                   pcount = params.length - 1;
2712                   arrayIdentifiers.push(identifiers.length);
2713                 } else {
2714                   params.push("%" + identifiers.length + "$I");
2715                   pcount = params.length - 1;
2716                 }
2717               }
2718
2719               /* Add persional privs array search. */
2720               if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
2721                 || (prop.attr && prop.attr.type === 'Array') || isArray)) {
2722
2723                 /* e.g. 'admin' = ANY (usernames_array) */
2724                 arrayParams = "";
2725                 params[pcount] += ' ' + op + ' ANY (';
2726
2727                 /* Build path. e.g. ((%1$I).%2$I).%3$I */
2728                 for (var f =0; f < arrayIdentifiers.length; f++) {
2729                   arrayParams += '%' + arrayIdentifiers[f] + '$I';
2730                   if (f < arrayIdentifiers.length - 1) {
2731                     arrayParams = "(" + arrayParams + ").";
2732                   }
2733                 }
2734                 params[pcount] += arrayParams + ')';
2735
2736               /* Add optional is null clause. */
2737               } else if (parameters[i].includeNull) {
2738                 /* e.g. %1$I = $1 or %1$I is null */
2739                 params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
2740               } else {
2741                 /* e.g. %1$I = $1 */
2742                 params[pcount] += " " + op + ' $' + count;
2743               }
2744
2745               orClause.push(params[pcount]);
2746             }
2747
2748             /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
2749             clauses.push('(' + orClause.join(' or ') + ')');
2750             count++;
2751             ret.parameters.push(param.value);
2752           }
2753         }
2754       }
2755
2756       ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
2757
2758       /* Massage ordeBy with quoted identifiers. */
2759       if (orderBy) {
2760         for (var i = 0; i < orderBy.length; i++) {
2761           /* Handle path case. */
2762           if (orderBy[i].attribute.indexOf('.') > -1) {
2763             parts = orderBy[i].attribute.split('.');
2764             prevOrm = orm;
2765             orderByParams.push("");
2766             pcount = orderByParams.length - 1;
2767
2768             for (var n = 0; n < parts.length; n++) {
2769               prop = XT.Orm.getProperty(orm, parts[n]);
2770               if (!prop) {
2771                 plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
2772               }
2773               orderByIdentifiers.push(parts[n]);
2774               orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
2775
2776               if (n < parts.length - 1) {
2777                 orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
2778                 orm = this.fetchOrm(nameSpace, prop.toOne.type);
2779               }
2780             }
2781             orm = prevOrm;
2782           /* Normal case. */
2783           } else {
2784             prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
2785             if (!prop) {
2786               plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
2787             }
2788             orderByIdentifiers.push(orderBy[i].attribute);
2789             orderByParams.push("%" + orderByIdentifiers.length + "$I");
2790             pcount = orderByParams.length - 1;
2791           }
2792
2793           if (orderBy[i].isEmpty) {
2794             orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
2795           }
2796           if (orderBy[i].descending) {
2797             orderByParams[pcount] += " desc";
2798           }
2799
2800           list.push(orderByParams[pcount])
2801         }
2802       }
2803
2804       ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
2805
2806       return ret;
2807     },
2808     /**
2809      * Renew a lock. Defaults to rewing the lock for 30 seconds.
2810      *
2811      * @param {Number} Key
2812      * @params {Object} Options: timeout
2813      * @returns {Date} New expiration or false.
2814      */
2815     renewLock: function (key, options) {
2816       var expires = new Date(),
2817         query,
2818         selectSql = "select * from xt.lock where lock_id = $1;",
2819         timeout = options && options.timeout ? options.timeout : 30,
2820         updateSql = "update xt.lock set lock_expires = $1 where lock_id = $2;";
2821
2822       if (typeof key !== "number") { return false; }
2823       expires = new Date(expires.setSeconds(expires.getSeconds() + timeout));
2824
2825       if (DEBUG) {
2826         XT.debug('renewLock sql = ', selectSql);
2827         XT.debug('renewLock values = ', [key]);
2828       }
2829       query = plv8.execute(selectSql, [key]);
2830
2831       if (query.length) {
2832         if (DEBUG) {
2833           XT.debug('renewLock sql = ', updateSql);
2834           XT.debug('renewLock values = ', [expires, key]);
2835         }
2836         plv8.execute(updateSql, [expires, key]);
2837
2838         return true;
2839       }
2840
2841       return false;
2842     }
2843   }
2844
2845 }());
2846
2847 $$ );