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