Merge branch 'remitto' into xtdata
authorBen Thompson <ben@xtuple.com>
Mon, 7 Apr 2014 17:08:50 +0000 (13:08 -0400)
committerBen Thompson <ben@xtuple.com>
Mon, 7 Apr 2014 17:08:50 +0000 (13:08 -0400)
enyo-client/application/source/views/workspace.js
lib/orm/source/manifest.js
lib/orm/source/xm/javascript/model.sql
lib/orm/source/xt/functions/any_uuid.sql [new file with mode: 0644]
lib/orm/source/xt/javascript/data.sql
lib/orm/source/xt/javascript/discovery.sql
lib/orm/source/xt/operators/any_uuid.sql [new file with mode: 0644]
node-datasource/lib/query/rest_query.js
test/database/joins.js [new file with mode: 0644]

index cb8229a..0386689 100644 (file)
@@ -1978,7 +1978,7 @@ strict: false*/
       if (customer) {
         this.$.customerShiptoWidget.setDisabled(false);
         this.$.customerShiptoWidget.addParameter({
-          attribute: "customer",
+          attribute: "customer.number",
           value: customer.id
         });
         if (this.$.creditCardWidget) {
index c3c09cb..4f5dedc 100644 (file)
@@ -13,6 +13,7 @@
     "xt/functions/add_primary_key.sql",
     "xt/functions/any_numeric.sql",
     "xt/functions/any_text.sql",
+    "xt/functions/any_uuid.sql",
     "xt/functions/begins_with.sql",
     "xt/functions/commit_record.sql",
     "xt/functions/create_table.sql",
@@ -46,6 +47,7 @@
     "xt/trigger_functions/record_did_change.sql",
     "xt/operators/any_numeric.sql",
     "xt/operators/any_text.sql",
+    "xt/operators/any_uuid.sql",
     "xt/operators/begins_with.sql",
     "xt/operators/ends_with.sql",
     "xt/operators/not_any_numeric.sql",
index 77de85c..e653b4c 100644 (file)
@@ -154,6 +154,353 @@ select xt.install_js('XM','Model','xtuple', $$
     return result ? result.id : 0;
   };
 
+  /**
+   Returns a complex query's results.
+   Sample usage:
+    select xt.post('{
+      "username": "admin",
+      "nameSpace": "XM",
+      "type": "Model",
+      "dispatch":{
+        "functionName":"query",
+        "parameters":[
+          "Address", {"query": {"parameters": [{"attribute": "city","operator": "=","value": "Norfolk"}]}}
+        ]
+      }
+    }');
+
+   @param {String} recordType to query
+   @param {Object} options: query
+   @returns Object
+  */
+  XM.Model.query = function (recordType, options) {
+    options = options || {};
+    var query = {};
+
+    if (recordType && options && options.query) {
+      query.username = XT.username;
+      query.nameSpace = 'XM';
+      query.type = recordType;
+      query.query = options.query;
+    }
+
+    result = XT.Rest.get(query);
+
+    return result;
+  };
+  XM.Model.query.scope = "Model";
+  XM.Model.query.description = "Perform an complex query on a resource. This allows you to use a POST body for the query vs. a long URL.";
+  XM.Model.query.request = {
+    "$ref": "Query"
+  };
+  XM.Model.query.parameterOrder = ["recordType", "options"];
+  // For JSON-Schema deff, see:
+  // https://github.com/fge/json-schema-validator/issues/46#issuecomment-14681103
+  XM.Model.query.schema = {
+    Query: {
+      properties: {
+        attributes: {
+          title: "Service request attributes",
+          description: "An array of attributes needed to perform a complex query.",
+          type: "array",
+          items: [
+            {
+              title: "Resource",
+              description: "The resource to query.",
+              type: "string",
+              required: true
+            },
+            {
+              title: "Options",
+              type: "object",
+              "$ref": "QueryOptions"
+            }
+          ],
+          "minItems": 2,
+          "maxItems": 2,
+          required: true
+        }
+      }
+    },
+    QueryOptions: {
+      properties: {
+        query: {
+          title: "query",
+          description: "The query to perform.",
+          type: "object",
+          "$ref": "QueryOptionsQuery"
+        }
+      }
+    },
+    QueryOptionsQuery: {
+      properties: {
+        parameters: {
+          title: "Parameters",
+          description: "The query parameters.",
+          type: "array",
+          items: [
+            {
+              title: "Attribute",
+              type: "object",
+              "$ref": "QueryOptionsParameters"
+            }
+          ],
+          "minItems": 1
+        },
+        orderBy: {
+          title: "Order By",
+          description: "The query order by.",
+          type: "array",
+          items: [
+            {
+              title: "Attribute",
+              type: "object",
+              "$ref": "QueryOptionsOrderBy"
+            }
+          ]
+        },
+        rowlimit: {
+          title: "Row Limit",
+          description: "The query for paged results.",
+          type: "integer"
+        },
+        maxresults: {
+          title: "Max Results",
+          description: "The query limit for total results.",
+          type: "integer"
+        },
+        pagetoken: {
+          title: "Page Token",
+          description: "The query offset page token.",
+          type: "integer"
+        },
+        count: {
+          title: "Count",
+          description: "Set to true to return only the count of results for this query.",
+          type: "boolean"
+        }
+      }
+    },
+    QueryOptionsParameters: {
+      properties: {
+        attribute: {
+          title: "Attribute",
+          description: "The column name used to construct the query's WHERE clasues.",
+          type: "string",
+          required: true
+        },
+        operator: {
+          title: "Operator",
+          description: "The operator used to construct the query's WHERE clasues.",
+          type: "string"
+        },
+        value: {
+          title: "Value",
+          description: "The value used to construct the query's WHERE clasues.",
+          required: true
+        }
+      }
+    },
+    QueryOptionsOrderBy: {
+      properties: {
+        attribute: {
+          title: "Attribute",
+          description: "The column name used to construct the query's ORDER BY.",
+          type: "string",
+          required: true
+        },
+        descending: {
+          title: "Direction",
+          description: "Set to true so the query's ORDER BY will be DESC.",
+          type: "boolean"
+        }
+      }
+    }
+  };
+
+  /**
+   Returns a complex query's results using the REST query structure. This is a
+   wrapper for XM.Model.query that reformats the query structure from a
+   rest_query to our XT.Rest structure. This dispatch function can be used by
+   a REST API client to query a resource when the query would be too long to
+   pass to the API as a GET URL query.
+
+   Sample usage:
+    select xt.post('{
+      "username": "admin",
+      "nameSpace": "XM",
+      "type": "Model",
+      "dispatch":{
+        "functionName":"restQuery",
+        "parameters":["Address", {"query": [{"city":{"EQUALS":"Norfolk"}}], "orderby": [{"ASC": "line1"}, {"DESC": "line2"}]}]
+      }
+    }');
+
+   @param {String} recordType to query
+   @param {Object} options: query
+   @returns Object
+  */
+  XM.Model.restQuery = function (recordType, options) {
+    options = options || {};
+    var order = {},
+        param = {},
+        query = {},
+        mapOperator = function (op) {
+          var operators = {
+                value: {
+                  ANY:          'ANY',
+                  NOT_ANY:      'NOT ANY',
+                  EQUALS:       '=',
+                  NOT_EQUALS:   '!=',
+                  LESS_THAN:    '<',
+                  AT_MOST:      '<=',
+                  GREATER_THAN: '>',
+                  AT_LEAST:     '>=',
+                  MATCHES:      'MATCHES',
+                  BEGINS_WITH:  'BEGINS_WITH'
+                }
+              };
+
+          return operators.value[op];
+        };
+
+    /* Convert from rest_query to XM.Model.query structure. */
+    if (recordType && options) {
+      if (options.query) {
+        query.parameters = [];
+        for (var i = 0; i < options.query.length; i++) {
+          for (var column in options.query[i]) {
+            for (var op in options.query[i][column]) {
+              param = {};
+              param.attribute = column;
+              param.operator = mapOperator(op);
+              param.value = options.query[i][column][op];
+              query.parameters.push(param);
+            }
+          }
+        }
+      }
+      if (options.orderby || options.orderBy) {
+        options.orderBy = options.orderby || options.orderBy;
+        query.orderBy = [];
+        for (var o = 0; o < options.orderBy.length; o++) {
+          for (var column in options.orderBy[o]) {
+            order = {};
+            order.attribute = options.orderBy[o][column];
+            if (column === 'DESC') {
+              order.descending = true;
+            }
+            query.orderBy.push(order);
+          }
+        }
+      }
+      if (options.rowlimit || options.rowLimit) {
+        options.rowLimit = options.rowlimit || options.rowLimit;
+        query.rowLimit = options.rowLimit;
+      }
+      if (options.maxresults || options.maxResults) {
+        options.maxResults = options.maxresults || options.maxResults;
+        query.rowLimit = options.maxResults;
+      }
+      if (options.pagetoken || options.pageToken) {
+        options.pageToken = options.pagetoken || options.pageToken;
+        if (query.rowLimit) {
+          query.rowOffset = (options.pageToken || 0) * (query.rowLimit);
+        } else {
+          query.rowOffset = (options.pageToken || 0);
+        }
+      }
+      if (options.count) {
+        query.count = options.count;
+      }
+    }
+
+    result = XM.Model.query(recordType, {"query": query});
+
+    return result;
+  };
+  XM.Model.restQuery.description = "Perform an complex query on a resource using the REST query structure. This allows you to use a POST body for the query vs. a long URL.";
+  XM.Model.restQuery.request = {
+    "$ref": "RestQuery"
+  };
+  XM.Model.restQuery.parameterOrder = ["recordType", "options"];
+  // For JSON-Schema deff, see:
+  // https://github.com/fge/json-schema-validator/issues/46#issuecomment-14681103
+  XM.Model.restQuery.schema = {
+    RestQuery: {
+      properties: {
+        attributes: {
+          title: "Service request attributes",
+          description: "An array of attributes needed to perform a complex query.",
+          type: "array",
+          items: [
+            {
+              title: "Resource",
+              description: "The resource to query.",
+              type: "string",
+              required: true
+            },
+            {
+              title: "Options",
+              type: "object",
+              "$ref": "RestQueryOptions"
+            }
+          ],
+          "minItems": 2,
+          "maxItems": 2,
+          required: true
+        }
+      }
+    },
+    RestQueryOptions: {
+      properties: {
+        query: {
+          title: "query",
+          description: "The query to perform.",
+          type: "array",
+          items: [
+            {
+              title: "column",
+              type: "object"
+            }
+          ],
+          "minItems": 1
+        },
+        orderby: {
+          title: "Order By",
+          description: "The query order by.",
+          type: "array",
+          items: [
+            {
+              title: "column",
+              type: "object"
+            }
+          ]
+        },
+        rowlimit: {
+          title: "Row Limit",
+          description: "The query for paged results.",
+          type: "integer"
+        },
+        maxresults: {
+          title: "Max Results",
+          description: "The query limit for total results.",
+          type: "integer"
+        },
+        pagetoken: {
+          title: "Page Token",
+          description: "The query offset page token.",
+          type: "integer"
+        },
+        count: {
+          title: "Count",
+          description: "Set to true to return only the count of results for this query.",
+          type: "boolean"
+        }
+      }
+    }
+  };
+
   /**
     Used to determine whether a model is used or not.
 
@@ -178,9 +525,9 @@ select xt.install_js('XM','Model','xtuple', $$
       attr,
       seq,
       tableName,
-      fkIndex, 
-      fkey, 
-      propIndex, 
+      fkIndex,
+      fkey,
+      propIndex,
       probObj;
 
     if (nkey) {
@@ -237,7 +584,7 @@ select xt.install_js('XM','Model','xtuple', $$
 
     if (DEBUG) { XT.debug('XM.Model.used keys length:', fkeys.length) }
     if (DEBUG) { XT.debug('XM.Model.used keys:', JSON.stringify(fkeys)) }
-    
+
     for (i = 0; i < fkeys.length; i++) {
       /* Validate */
 
@@ -268,5 +615,5 @@ select xt.install_js('XM','Model','xtuple', $$
   XM.Model.used = function(recordType, id) {
     return XM.PrivateModel.used(recordType, id);
   };
-  
+
 $$ );
diff --git a/lib/orm/source/xt/functions/any_uuid.sql b/lib/orm/source/xt/functions/any_uuid.sql
new file mode 100644 (file)
index 0000000..0f20681
--- /dev/null
@@ -0,0 +1,3 @@
+create or replace function xt.any_uuid(arg1 uuid, arg2 uuid[]) returns boolean immutable as $$
+  select array[$1] <@ $2;
+$$ language 'sql';
index 689ede4..b031ffb 100644 (file)
@@ -33,30 +33,41 @@ select xt.install_js('XT','Data','xtuple', $$
      * @param {Array} Parameters - optional
      * @returns {Object}
      */
-    buildClause: function (nameSpace, type, parameters, orderBy) {
+    buildClauseOptimized: function (nameSpace, type, parameters, orderBy) {
       parameters = parameters || [];
 
-      var arrayIdentifiers = [],
+      var that = this,
+        arrayIdentifiers = [],
         arrayParams,
         charSql,
         childOrm,
         clauses = [],
         count = 1,
+        fromKeyProp,
+        groupByColumnParams = [],
         identifiers = [],
-        list = [],
+        joinIdentifiers = [],
+        orderByList = [],
+        orderByColumnList = [],
         isArray = false,
         op,
         orClause,
         orderByIdentifiers = [],
+        orderByColumnIdentifiers = [],
         orderByParams = [],
+        orderByColumnParams = [],
+        joins = [],
         orm = this.fetchOrm(nameSpace, type),
         param,
         params = [],
         parts,
         pcount,
+        pertinentExtension,
+        pgType,
         prevOrm,
         privileges = orm.privileges,
         prop,
+        sourceTableAlias,
         ret = {};
 
       ret.conditions = "";
@@ -83,6 +94,45 @@ select xt.install_js('XT','Data','xtuple', $$
         });
       }
 
+      /* Support the short cut wherein the client asks for a filter on a toOne with a
+        string. Technically they should use "theAttr.theAttrNaturalKey", but if they
+        don't, massage the inputs as if they did */
+      parameters.map(function (parameter) {
+        var attributeIsString = typeof parameter.attribute === 'string';
+          attributes = attributeIsString ? [parameter.attribute] : parameter.attribute;
+
+        attributes.map(function (attribute) {
+          var prop = XT.Orm.getProperty(orm, attribute),
+            propName = prop.name,
+            childOrm,
+            naturalKey,
+            index;
+
+          if ((prop.toOne || prop.toMany) && attribute.indexOf('.') < 0) {
+            /* Someone is querying on a toOne without using a path */
+            /* TODO: even if there's a path x.y, it's possible that it's still not
+              correct because the correct path maybe is x.y.naturalKeyOfY */
+            if (prop.toOne && prop.toOne.type) {
+              childOrm = that.fetchOrm(nameSpace, prop.toOne.type);
+            } else if (prop.toMany && prop.toMany.type) {
+              childOrm = that.fetchOrm(nameSpace, prop.toMany.type);
+            } else {
+              plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
+            }
+            naturalKey = XT.Orm.naturalKey(childOrm);
+            if (attributeIsString) {
+              /* add the natural key to the end of the requested attribute */
+              parameter.attribute = attribute + "." + naturalKey;
+            } else {
+              /* swap out the attribute in the array for the one with the prepended natural key */
+              index = parameter.attribute.indexOf(attribute);
+              parameter.attribute.splice(index, 1);
+              parameter.attribute.push(attribute + "." + naturalKey);
+            }
+          }
+        });
+      });
+
       /* Handle parameters. */
       if (parameters.length) {
         for (var i = 0; i < parameters.length; i++) {
@@ -146,13 +196,14 @@ select xt.install_js('XT','Data','xtuple', $$
             prop = XT.Orm.getProperty(orm, 'characteristics');
 
             /* Build the characteristics query clause. */
+            identifiers.push(XT.Orm.primaryKey(orm, true));
             identifiers.push(prop.toMany.inverse);
             identifiers.push(orm.nameSpace.toLowerCase());
             identifiers.push(prop.toMany.type.decamelize());
             identifiers.push(param.attribute);
             identifiers.push(param.value);
 
-            charSql = 'id in (' +
+            charSql = '%' + (identifiers.length - 5) + '$I in (' +
                       '  select %' + (identifiers.length - 4) + '$I '+
                       '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
                       '    join char on (char_name = characteristic)' +
@@ -180,19 +231,46 @@ select xt.install_js('XT','Data','xtuple', $$
                   plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
                 }
 
-                /* Build path. e.g. ((%1$I).%2$I).%3$I */
-                identifiers.push(parts[n]);
-                params[pcount] += "%" + identifiers.length + "$I";
-                if (n < parts.length - 1) {
-                  params[pcount] = "(" + params[pcount] + ").";
-                  childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                /* Build path. */
+                if (n === parts.length - 1) {
+                  identifiers.push("jt" + (joins.length - 1));
+                  identifiers.push(prop.attr.column);
+                  pgType = this.getPgTypeFromOrmType(
+                    this.getNamespaceFromNamespacedTable(childOrm.table),
+                    this.getTableFromNamespacedTable(childOrm.table),
+                    prop.attr.column
+                  );
+                  pgType = pgType ? "::" + pgType + "[]" : '';
+                  params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
+                  params[pcount] += ' ' + op + ' ARRAY[' + param.value.join(',') + ']' + pgType;
                 } else {
-                  params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
+                  childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                  sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+                  joinIdentifiers.push(
+                    this.getNamespaceFromNamespacedTable(childOrm.table),
+                    this.getTableFromNamespacedTable(childOrm.table),
+                    sourceTableAlias, prop.toOne.column,
+                    XT.Orm.primaryKey(childOrm, true));
+                  joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+                    + "$I jt" + joins.length + " on %"
+                    + (joinIdentifiers.length - 2) + "$I.%"
+                    + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
                 }
               }
             } else {
-              identifiers.push(param.attribute);
-              params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
+              prop = XT.Orm.getProperty(orm, param.attribute);
+              if (!prop) {
+                plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
+              }
+              identifiers.push("t1");
+              identifiers.push(prop.attr.column);
+              pgType = this.getPgTypeFromOrmType(
+                this.getNamespaceFromNamespacedTable(orm.table),
+                this.getTableFromNamespacedTable(orm.table),
+                prop.attr.column
+              );
+              pgType = pgType ? "::" + pgType + "[]" : '';
+              params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']' + pgType);
               pcount = params.length - 1;
             }
             clauses.push(params[pcount]);
@@ -221,7 +299,13 @@ select xt.install_js('XT','Data','xtuple', $$
                   }
 
                   if (m < parts.length - 1) {
-                    childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                    if (prop.toOne && prop.toOne.type) {
+                      childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                    } else if (prop.toMany && prop.toMany.type) {
+                      childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
+                    } else {
+                      plv8.elog(ERROR, "toOne or toMany property is missing it's 'type': " + prop.name);
+                    }
                   } else if (prop.attr && prop.attr.type === 'Array') {
                     /* The last property in the path is an array. */
                     isArray = true;
@@ -241,33 +325,69 @@ select xt.install_js('XT','Data','xtuple', $$
 
                   /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
                   if (param.isUsernamePrivFilter && isArray) {
-                    identifiers.push(parts[n]);
+                    identifiers.push(prop.attr.column);
                     arrayIdentifiers.push(identifiers.length);
 
                     if (n < parts.length - 1) {
                       childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
                     }
                   } else {
-                    /* Build path. e.g. ((%1$I).%2$I).%3$I */
-                    identifiers.push(parts[n]);
-                    params[pcount] += "%" + identifiers.length + "$I";
-
-                    if (n < parts.length - 1) {
-                      params[pcount] = "(" + params[pcount] + ").";
-                      childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
-                    } else if (param.isLower) {
-                      params[pcount] = "lower(" + params[pcount] + ")";
+                    /* Build path, e.g. table_name.column_name */
+                    if (n === parts.length - 1) {
+                      identifiers.push("jt" + (joins.length - 1));
+                      identifiers.push(prop.attr.column);
+                      params[pcount] += "%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I";
+                      if (param.isLower) {
+                        params[pcount] = "lower(" + params[pcount] + ")";
+                      }
+                    } else {
+                      sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+                      if (prop.toOne && prop.toOne.type) {
+                        childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                        joinIdentifiers.push(
+                          this.getNamespaceFromNamespacedTable(childOrm.table),
+                          this.getTableFromNamespacedTable(childOrm.table),
+                          sourceTableAlias, prop.toOne.column,
+                          XT.Orm.primaryKey(childOrm, true)
+                        );
+                      } else if (prop.toMany && prop.toMany.type) {
+                        childOrm = this.fetchOrm(nameSpace, prop.toMany.type);
+                        joinIdentifiers.push(
+                          this.getNamespaceFromNamespacedTable(childOrm.table),
+                          this.getTableFromNamespacedTable(childOrm.table),
+                          sourceTableAlias, prop.toMany.column,
+                          XT.Orm.primaryKey(childOrm, true)
+                        );
+                      }
+                      joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+                        + "$I jt" + joins.length + " on %"
+                        + (joinIdentifiers.length - 2) + "$I.%"
+                        + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
                     }
                   }
                 }
               } else {
                 /* Validate attribute. */
                 prop = XT.Orm.getProperty(orm, param.attribute[c]);
+                pertinentExtension = XT.Orm.getProperty(orm, param.attribute[c], true);
+                if(pertinentExtension.isChild) {
+                  /* We'll need to join this orm extension */
+                  fromKeyProp = XT.Orm.getProperty(orm, pertinentExtension.relations[0].inverse);
+                  joinIdentifiers.push(
+                    this.getNamespaceFromNamespacedTable(pertinentExtension.table),
+                    this.getTableFromNamespacedTable(pertinentExtension.table),
+                    fromKeyProp.attr.column,
+                    pertinentExtension.relations[0].column);
+                  joins.push("left join %" + (joinIdentifiers.length - 3) + "$I.%" + (joinIdentifiers.length - 2)
+                    + "$I jt" + joins.length + " on t1.%"
+                    + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
+                }
                 if (!prop) {
                   plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
                 }
 
-                identifiers.push(param.attribute[c]);
+                identifiers.push(pertinentExtension.isChild ? "jt" + (joins.length - 1) : "t1");
+                identifiers.push(prop.attr.column);
 
                 /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
                 if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
@@ -277,7 +397,7 @@ select xt.install_js('XT','Data','xtuple', $$
                   pcount = params.length - 1;
                   arrayIdentifiers.push(identifiers.length);
                 } else {
-                  params.push("%" + identifiers.length + "$I");
+                  params.push("%" + (identifiers.length - 1) + "$I.%" + identifiers.length + "$I");
                   pcount = params.length - 1;
                 }
               }
@@ -286,6 +406,7 @@ select xt.install_js('XT','Data','xtuple', $$
               if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
                 || (prop.attr && prop.attr.type === 'Array') || isArray)) {
 
+                /* XXX: this bit of code has not been touched by the optimization refactor */
                 /* e.g. 'admin' = ANY (usernames_array) */
                 arrayParams = "";
                 params[pcount] += ' ' + op + ' ANY (';
@@ -321,7 +442,9 @@ select xt.install_js('XT','Data','xtuple', $$
 
       ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
 
-      /* Massage ordeBy with quoted identifiers. */
+      /* Massage orderBy with quoted identifiers. */
+      /* We need to support the xm case for sql2 and the xt/public (column) optimized case for sql1 */
+      /* In practice we build the two lists independently of one another */
       if (orderBy) {
         for (var i = 0; i < orderBy.length; i++) {
           /* Handle path case. */
@@ -329,6 +452,8 @@ select xt.install_js('XT','Data','xtuple', $$
             parts = orderBy[i].attribute.split('.');
             prevOrm = orm;
             orderByParams.push("");
+            orderByColumnParams.push("");
+            groupByColumnParams.push("");
             pcount = orderByParams.length - 1;
 
             for (var n = 0; n < parts.length; n++) {
@@ -339,9 +464,24 @@ select xt.install_js('XT','Data','xtuple', $$
               orderByIdentifiers.push(parts[n]);
               orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
 
-              if (n < parts.length - 1) {
+              if (n === parts.length - 1) {
+                orderByColumnIdentifiers.push("jt" + (joins.length - 1));
+                orderByColumnIdentifiers.push(prop.attr.column);
+                orderByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
+                groupByColumnParams[pcount] += "%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I"
+              } else {
                 orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
                 orm = this.fetchOrm(nameSpace, prop.toOne.type);
+                sourceTableAlias = n === 0 ? "t1" : "jt" + joins.length - 1;
+                joinIdentifiers.push(
+                  this.getNamespaceFromNamespacedTable(orm.table),
+                  this.getTableFromNamespacedTable(orm.table),
+                  sourceTableAlias, prop.toOne.column,
+                  XT.Orm.primaryKey(orm, true));
+                joins.push("left join %" + (joinIdentifiers.length - 4) + "$I.%" + (joinIdentifiers.length - 3)
+                  + "$I jt" + joins.length + " on %"
+                  + (joinIdentifiers.length - 2) + "$I.%"
+                  + (joinIdentifiers.length - 1) + "$I = jt" + joins.length + ".%" + joinIdentifiers.length + "$I");
               }
             }
             orm = prevOrm;
@@ -352,22 +492,32 @@ select xt.install_js('XT','Data','xtuple', $$
               plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
             }
             orderByIdentifiers.push(orderBy[i].attribute);
+            orderByColumnIdentifiers.push("t1");
+            orderByColumnIdentifiers.push(prop.attr.column);
             orderByParams.push("%" + orderByIdentifiers.length + "$I");
+            orderByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
+            groupByColumnParams.push("%" + (orderByColumnIdentifiers.length - 1) + "$I.%" + orderByColumnIdentifiers.length + "$I");
             pcount = orderByParams.length - 1;
           }
 
           if (orderBy[i].isEmpty) {
             orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
+            orderByColumnParams[pcount] = "length(" + orderByColumnParams[pcount] + ")=0";
           }
           if (orderBy[i].descending) {
             orderByParams[pcount] += " desc";
+            orderByColumnParams[pcount] += " desc";
           }
 
-          list.push(orderByParams[pcount])
+          orderByList.push(orderByParams[pcount])
+          orderByColumnList.push(orderByColumnParams[pcount])
         }
       }
 
-      ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
+      ret.orderBy = orderByList.length ? XT.format('order by ' + orderByList.join(','), orderByIdentifiers) : '';
+      ret.orderByColumns = orderByColumnList.length ? XT.format('order by ' + orderByColumnList.join(','), orderByColumnIdentifiers) : '';
+      ret.groupByColumns = groupByColumnParams.length ? XT.format(', ' + groupByColumnParams.join(','), orderByColumnIdentifiers) : '';
+      ret.joins = joins.length ? XT.format(joins.join(' '), joinIdentifiers) : '';
 
       return ret;
     },
@@ -1507,23 +1657,18 @@ select xt.install_js('XT','Data','xtuple', $$
      * @returns {Number}
      */
     getTableOid: function (table) {
-      var name = table.toLowerCase(), /* be generous */
-        namespace = "public", /* default assumed if no dot in name */
+      var tableName = this.getTableFromNamespacedTable(table).toLowerCase(), /* be generous */
+        namespace = this.getNamespaceFromNamespacedTable(table),
         ret,
         sql = "select pg_class.oid::integer as oid " +
              "from pg_class join pg_namespace on relnamespace = pg_namespace.oid " +
              "where relname = $1 and nspname = $2";
 
-      if (table.indexOf(".") > 0) {
-        namespace = table.beforeDot();
-        table = table.afterDot();
-      }
-
       if (DEBUG) {
         XT.debug('getTableOid sql =', sql);
-        XT.debug('getTableOid values =', [table, namespace]);
+        XT.debug('getTableOid values =', [tableName, namespace]);
       }
-      ret = plv8.execute(sql, [table, namespace])[0].oid - 0;
+      ret = plv8.execute(sql, [tableName, namespace])[0].oid - 0;
 
       // TODO - Handle not found error.
 
@@ -1568,6 +1713,34 @@ select xt.install_js('XT','Data','xtuple', $$
       }
     },
 
+    getNamespaceFromNamespacedTable: function (fullName) {
+      return fullName.indexOf(".") > 0 ? fullName.beforeDot() : "public";
+    },
+
+    getTableFromNamespacedTable: function (fullName) {
+      return fullName.indexOf(".") > 0 ? fullName.afterDot() : fullName;
+    },
+
+    getPgTypeFromOrmType: function (schema, table, column) {
+      var sql = "select data_type from information_schema.columns " +
+                "where true " +
+                "and table_schema = $1 " +
+                "and table_name = $2 " +
+                "and column_name = $3;",
+          pgType,
+          values = [schema, table, column];
+
+      if (DEBUG) {
+        XT.debug('getPgTypeFromOrmType sql =', sql);
+        XT.debug('getPgTypeFromOrmType values =', values);
+      }
+
+      pgType = plv8.execute(sql, values);
+      pgType = pgType ? pgType[0].data_type : false;
+
+      return pgType;
+    },
+
     /**
      * Get the natural key id for an object based on a passed in primary key.
      *
@@ -1663,10 +1836,13 @@ select xt.install_js('XT','Data','xtuple', $$
         encryptionKey = options.encryptionKey,
         orderBy = query.orderBy,
         orm = this.fetchOrm(nameSpace, type),
+        table,
+        tableNamespace,
         parameters = query.parameters,
-        clause = this.buildClause(nameSpace, type, parameters, orderBy),
+        clause = this.buildClauseOptimized(nameSpace, type, parameters, orderBy),
         i,
         pkey = XT.Orm.primaryKey(orm),
+        pkeyColumn = XT.Orm.primaryKey(orm, true),
         nkey = XT.Orm.naturalKey(orm),
         limit = query.rowLimit ? XT.format('limit %1$L', [query.rowLimit]) : '',
         offset = query.rowOffset ? XT.format('offset %1$L', [query.rowOffset]) : '',
@@ -1682,19 +1858,21 @@ select xt.install_js('XT','Data','xtuple', $$
         sqlCount,
         etags,
         sql_etags,
-        etag_namespace,
-        etag_table,
-        sql1 = 'select %3$I as id from %1$I.%2$I where {conditions} {orderBy} {limit} {offset};',
+        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};',
         sql2 = 'select * from %1$I.%2$I where %3$I in ({ids}) {orderBy}';
 
       /* Validate - don't bother running the query if the user has no privileges. */
       if (!this.checkPrivileges(nameSpace, type)) { return []; }
 
+      tableNamespace = this.getNamespaceFromNamespacedTable(orm.table);
+      table = this.getTableFromNamespacedTable(orm.table);
+
       if (query.count) {
         /* Just get the count of rows that match the conditions */
-        sqlCount = 'select count(*) as count from %1$I.%2$I where {conditions};';
-        sqlCount = XT.format(sqlCount, [nameSpace.decamelize(), type.decamelize()]);
-        sqlCount = sqlCount.replace('{conditions}', clause.conditions);
+        sqlCount = 'select count(distinct t1.%3$I) as count from %1$I.%2$I t1 {joins} where {conditions};';
+        sqlCount = XT.format(sqlCount, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
+        sqlCount = sqlCount.replace('{joins}', clause.joins)
+                           .replace('{conditions}', clause.conditions);
 
         if (DEBUG) {
           XT.debug('fetch sqlCount = ', sqlCount);
@@ -1706,9 +1884,11 @@ select xt.install_js('XT','Data','xtuple', $$
       }
 
       /* Query the model. */
-      sql1 = XT.format(sql1, [nameSpace.decamelize(), type.decamelize(), pkey]);
-      sql1 = sql1.replace('{conditions}', clause.conditions)
-                 .replace(/{orderBy}/g, clause.orderBy)
+      sql1 = XT.format(sql1, [tableNamespace.decamelize(), table.decamelize(), pkeyColumn]);
+      sql1 = sql1.replace('{joins}', clause.joins)
+                 .replace('{conditions}', clause.conditions)
+                 .replace(/{groupBy}/g, clause.groupByColumns)
+                 .replace(/{orderBy}/g, clause.orderByColumns)
                  .replace('{limit}', limit)
                  .replace('{offset}', offset);
 
@@ -1729,14 +1909,6 @@ select xt.install_js('XT','Data','xtuple', $$
       });
 
       if (orm.lockable) {
-        if (orm.table.indexOf(".") > 0) {
-          etag_namespace = orm.table.beforeDot();
-          etag_table = orm.table.afterDot();
-        } else {
-          etag_namespace = 'public';
-          etag_table = orm.table;
-        }
-
         sql_etags = "select ver_etag as etag, ver_record_id as id " +
                     "from xt.ver " +
                     "where ver_table_oid = ( " +
@@ -1746,7 +1918,7 @@ select xt.install_js('XT','Data','xtuple', $$
                       "where nspname = %1$L and relname = %2$L " +
                     ") " +
                     "and ver_record_id in ({ids})";
-        sql_etags = XT.format(sql_etags, [etag_namespace, etag_table]);
+        sql_etags = XT.format(sql_etags, [tableNamespace, table]);
         sql_etags = sql_etags.replace('{ids}', idParams.join());
 
         if (DEBUG) {
@@ -1866,12 +2038,8 @@ select xt.install_js('XT','Data','xtuple', $$
         context.prop = XT.Orm.getProperty(context.map, context.relation);
         context.pertinentExtension = XT.Orm.getProperty(context.map, context.relation, true);
         context.underlyingTable = context.pertinentExtension.table,
-        context.underlyingNameSpace = context.underlyingTable.indexOf(".") > 0 ?
-          context.underlyingTable.beforeDot() :
-          "public";
-        context.underlyingType = context.underlyingTable.indexOf(".") > 0 ?
-          context.underlyingTable.afterDot() :
-          context.underlyingTable;
+        context.underlyingNameSpace = this.getNamespaceFromNamespacedTable(context.underlyingTable);
+        context.underlyingType = this.getTableFromNamespacedTable(context.underlyingTable);
         context.fkey = context.prop.toMany.inverse;
         context.fkeyColumn = context.prop.toMany.column;
         context.pkey = XT.Orm.naturalKey(context.map) || XT.Orm.primaryKey(context.map);
@@ -2266,6 +2434,346 @@ select xt.install_js('XT','Data','xtuple', $$
       return true;
     },
 
+    /* This deprecated function is still used by three dispatch functions. We should delete
+    this as soon as we refactor those, and then rename buildClauseOptimized to buildClause */
+    buildClause: function (nameSpace, type, parameters, orderBy) {
+      parameters = parameters || [];
+
+      var arrayIdentifiers = [],
+        arrayParams,
+        charSql,
+        childOrm,
+        clauses = [],
+        count = 1,
+        identifiers = [],
+        list = [],
+        isArray = false,
+        op,
+        orClause,
+        orderByIdentifiers = [],
+        orderByParams = [],
+        orm = this.fetchOrm(nameSpace, type),
+        param,
+        params = [],
+        parts,
+        pcount,
+        prevOrm,
+        privileges = orm.privileges,
+        prop,
+        ret = {};
+
+      ret.conditions = "";
+      ret.parameters = [];
+
+      /* Handle privileges. */
+      if (orm.isNestedOnly) { plv8.elog(ERROR, 'Access Denied'); }
+      if (privileges &&
+          (!privileges.all ||
+            (privileges.all &&
+              (!this.checkPrivilege(privileges.all.read) &&
+              !this.checkPrivilege(privileges.all.update)))
+          ) &&
+          privileges.personal &&
+          (this.checkPrivilege(privileges.personal.read) ||
+            this.checkPrivilege(privileges.personal.update))
+        ) {
+
+        parameters.push({
+          attribute: privileges.personal.properties,
+          isLower: true,
+          isUsernamePrivFilter: true,
+          value: XT.username
+        });
+      }
+
+      /* Handle parameters. */
+      if (parameters.length) {
+        for (var i = 0; i < parameters.length; i++) {
+          orClause = [];
+          param = parameters[i];
+          op = param.operator || '=';
+          switch (op) {
+          case '=':
+          case '>':
+          case '<':
+          case '>=':
+          case '<=':
+          case '!=':
+            break;
+          case 'BEGINS_WITH':
+            op = '~^';
+            break;
+          case 'ENDS_WITH':
+            op = '~?';
+            break;
+          case 'MATCHES':
+            op = '~*';
+            break;
+          case 'ANY':
+            op = '<@';
+            for (var c = 0; c < param.value.length; c++) {
+              ret.parameters.push(param.value[c]);
+              param.value[c] = '$' + count;
+              count++;
+            }
+            break;
+          case 'NOT ANY':
+            op = '!<@';
+            for (var c = 0; c < param.value.length; c++) {
+              ret.parameters.push(param.value[c]);
+              param.value[c] = '$' + count;
+              count++;
+            }
+            break;
+          default:
+            plv8.elog(ERROR, 'Invalid operator: ' + op);
+          }
+
+          /* Handle characteristics. This is very specific to xTuple,
+             and highly dependant on certain table structures and naming conventions,
+             but otherwise way too much work to refactor in an abstract manner right now. */
+          if (param.isCharacteristic) {
+            /* Handle array. */
+            if (op === '<@') {
+              param.value = ' ARRAY[' + param.value.join(',') + ']';
+            }
+
+            /* Booleans are stored as strings. */
+            if (param.value === true) {
+              param.value = 't';
+            } else if (param.value === false) {
+              param.value = 'f';
+            }
+
+            /* Yeah, it depends on a property called 'characteristics'... */
+            prop = XT.Orm.getProperty(orm, 'characteristics');
+
+            /* Build the characteristics query clause. */
+            identifiers.push(prop.toMany.inverse);
+            identifiers.push(orm.nameSpace.toLowerCase());
+            identifiers.push(prop.toMany.type.decamelize());
+            identifiers.push(param.attribute);
+            identifiers.push(param.value);
+
+            charSql = 'id in (' +
+                      '  select %' + (identifiers.length - 4) + '$I '+
+                      '  from %' + (identifiers.length - 3) + '$I.%' + (identifiers.length - 2) + '$I ' +
+                      '    join char on (char_name = characteristic)' +
+                      '  where 1=1 ' +
+                      /* Note: Not using $i for these. L = literal here. These is not identifiers. */
+                      '    and char_name = %' + (identifiers.length - 1) + '$L ' +
+                      '    and value ' + op + ' %' + (identifiers.length) + '$L ' +
+                      ')';
+
+            clauses.push(charSql);
+
+          /* Array comparisons handle another way. e.g. %1$I !<@ ARRAY[$1,$2] */
+          } else if (op === '<@' || op === '!<@') {
+            /* Handle paths if applicable. */
+            if (param.attribute.indexOf('.') > -1) {
+              parts = param.attribute.split('.');
+              childOrm = this.fetchOrm(nameSpace, type);
+              params.push("");
+              pcount = params.length - 1;
+
+              for (var n = 0; n < parts.length; n++) {
+                /* Validate attribute. */
+                prop = XT.Orm.getProperty(childOrm, parts[n]);
+                if (!prop) {
+                  plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
+                }
+
+                /* Build path. e.g. ((%1$I).%2$I).%3$I */
+                identifiers.push(parts[n]);
+                params[pcount] += "%" + identifiers.length + "$I";
+                if (n < parts.length - 1) {
+                  params[pcount] = "(" + params[pcount] + ").";
+                  childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                } else {
+                  params[pcount] += op + ' ARRAY[' + param.value.join(',') + ']';
+                }
+              }
+            } else {
+              identifiers.push(param.attribute);
+              params.push("%" + identifiers.length + "$I " + op + ' ARRAY[' + param.value.join(',') + ']');
+              pcount = params.length - 1;
+            }
+            clauses.push(params[pcount]);
+
+          /* Everything else handle another. */
+          } else {
+            if (XT.typeOf(param.attribute) !== 'array') {
+              param.attribute = [param.attribute];
+            }
+
+            for (var c = 0; c < param.attribute.length; c++) {
+              /* Handle paths if applicable. */
+              if (param.attribute[c].indexOf('.') > -1) {
+                parts = param.attribute[c].split('.');
+                childOrm = this.fetchOrm(nameSpace, type);
+                params.push("");
+                pcount = params.length - 1;
+                isArray = false;
+
+                /* Check if last part is an Array. */
+                for (var m = 0; m < parts.length; m++) {
+                  /* Validate attribute. */
+                  prop = XT.Orm.getProperty(childOrm, parts[m]);
+                  if (!prop) {
+                    plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[m]);
+                  }
+
+                  if (m < parts.length - 1) {
+                    childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                  } else if (prop.attr && prop.attr.type === 'Array') {
+                    /* The last property in the path is an array. */
+                    isArray = true;
+                    params[pcount] = '$' + count;
+                  }
+                }
+
+                /* Reset the childOrm to parent. */
+                childOrm = this.fetchOrm(nameSpace, type);
+
+                for (var n = 0; n < parts.length; n++) {
+                  /* Validate attribute. */
+                  prop = XT.Orm.getProperty(childOrm, parts[n]);
+                  if (!prop) {
+                    plv8.elog(ERROR, 'Attribute not found in object map: ' + parts[n]);
+                  }
+
+                  /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
+                  if (param.isUsernamePrivFilter && isArray) {
+                    identifiers.push(parts[n]);
+                    arrayIdentifiers.push(identifiers.length);
+
+                    if (n < parts.length - 1) {
+                      childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                    }
+                  } else {
+                    /* Build path. e.g. ((%1$I).%2$I).%3$I */
+                    identifiers.push(parts[n]);
+                    params[pcount] += "%" + identifiers.length + "$I";
+
+                    if (n < parts.length - 1) {
+                      params[pcount] = "(" + params[pcount] + ").";
+                      childOrm = this.fetchOrm(nameSpace, prop.toOne.type);
+                    } else if (param.isLower) {
+                      params[pcount] = "lower(" + params[pcount] + ")";
+                    }
+                  }
+                }
+              } else {
+                /* Validate attribute. */
+                prop = XT.Orm.getProperty(orm, param.attribute[c]);
+                if (!prop) {
+                  plv8.elog(ERROR, 'Attribute not found in object map: ' + param.attribute[c]);
+                }
+
+                identifiers.push(param.attribute[c]);
+
+                /* Do a persional privs array search e.g. 'admin' = ANY (usernames_array). */
+                if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested) ||
+                  (prop.attr && prop.attr.type === 'Array'))) {
+
+                  params.push('$' + count);
+                  pcount = params.length - 1;
+                  arrayIdentifiers.push(identifiers.length);
+                } else {
+                  params.push("%" + identifiers.length + "$I");
+                  pcount = params.length - 1;
+                }
+              }
+
+              /* Add persional privs array search. */
+              if (param.isUsernamePrivFilter && ((prop.toMany && !prop.isNested)
+                || (prop.attr && prop.attr.type === 'Array') || isArray)) {
+
+                /* e.g. 'admin' = ANY (usernames_array) */
+                arrayParams = "";
+                params[pcount] += ' ' + op + ' ANY (';
+
+                /* Build path. e.g. ((%1$I).%2$I).%3$I */
+                for (var f =0; f < arrayIdentifiers.length; f++) {
+                  arrayParams += '%' + arrayIdentifiers[f] + '$I';
+                  if (f < arrayIdentifiers.length - 1) {
+                    arrayParams = "(" + arrayParams + ").";
+                  }
+                }
+                params[pcount] += arrayParams + ')';
+
+              /* Add optional is null clause. */
+              } else if (parameters[i].includeNull) {
+                /* e.g. %1$I = $1 or %1$I is null */
+                params[pcount] = params[pcount] + " " + op + ' $' + count + ' or ' + params[pcount] + ' is null';
+              } else {
+                /* e.g. %1$I = $1 */
+                params[pcount] += " " + op + ' $' + count;
+              }
+
+              orClause.push(params[pcount]);
+            }
+
+            /* If more than one clause we'll get: (%1$I = $1 or %1$I = $2 or %1$I = $3) */
+            clauses.push('(' + orClause.join(' or ') + ')');
+            count++;
+            ret.parameters.push(param.value);
+          }
+        }
+      }
+
+      ret.conditions = (clauses.length ? '(' + XT.format(clauses.join(' and '), identifiers) + ')' : ret.conditions) || true;
+
+      /* Massage ordeBy with quoted identifiers. */
+      if (orderBy) {
+        for (var i = 0; i < orderBy.length; i++) {
+          /* Handle path case. */
+          if (orderBy[i].attribute.indexOf('.') > -1) {
+            parts = orderBy[i].attribute.split('.');
+            prevOrm = orm;
+            orderByParams.push("");
+            pcount = orderByParams.length - 1;
+
+            for (var n = 0; n < parts.length; n++) {
+              prop = XT.Orm.getProperty(orm, parts[n]);
+              if (!prop) {
+                plv8.elog(ERROR, 'Attribute not found in map: ' + parts[n]);
+              }
+              orderByIdentifiers.push(parts[n]);
+              orderByParams[pcount] += "%" + orderByIdentifiers.length + "$I";
+
+              if (n < parts.length - 1) {
+                orderByParams[pcount] = "(" + orderByParams[pcount] + ").";
+                orm = this.fetchOrm(nameSpace, prop.toOne.type);
+              }
+            }
+            orm = prevOrm;
+          /* Normal case. */
+          } else {
+            prop = XT.Orm.getProperty(orm, orderBy[i].attribute);
+            if (!prop) {
+              plv8.elog(ERROR, 'Attribute not found in map: ' + orderBy[i].attribute);
+            }
+            orderByIdentifiers.push(orderBy[i].attribute);
+            orderByParams.push("%" + orderByIdentifiers.length + "$I");
+            pcount = orderByParams.length - 1;
+          }
+
+          if (orderBy[i].isEmpty) {
+            orderByParams[pcount] = "length(" + orderByParams[pcount] + ")=0";
+          }
+          if (orderBy[i].descending) {
+            orderByParams[pcount] += " desc";
+          }
+
+          list.push(orderByParams[pcount])
+        }
+      }
+
+      ret.orderBy = list.length ? XT.format('order by ' + list.join(','), orderByIdentifiers) : '';
+
+      return ret;
+    },
     /**
      * Renew a lock. Defaults to rewing the lock for 30 seconds.
      *
index 567b778..a9b6694 100644 (file)
@@ -108,7 +108,7 @@ select xt.install_js('XT','Discovery','xtuple', $$
         listItemOrms = [],
         org = plv8.execute("select current_database()"),
         ormAuth = {},
-        orms = [],
+        orms,
         schemas = {},
         services,
         version = "v1alpha1";
@@ -121,7 +121,12 @@ select xt.install_js('XT','Discovery','xtuple', $$
       /* Build up ORMs from array. */
       for (var i = 0; i < orm.length; i++) {
         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
-        orms = orms.concat(gotOrms).unique();
+        if (gotOrms) {
+          if (!(orms instanceof Array)) {
+            orms = [];
+          }
+          orms = orms.concat(gotOrms).unique();
+        }
       }
     } else {
       orms = XT.Discovery.getIsRestORMs();
@@ -220,25 +225,13 @@ select xt.install_js('XT','Discovery','xtuple', $$
      * Auth section.
      */
     discovery.auth = XT.Discovery.getAuth(orm, rootUrl);
+    discovery.auth = XT.Discovery.getServicesAuth(orm, discovery.auth, rootUrl);
 
     /*
      * Schema section.
      */
     XT.Discovery.getORMSchemas(orms, schemas);
 
-    if (!schemas) {
-      return false;
-    }
-
-    /* Get parent ListItem ORMs */
-    for (var i = 0; i < orms.length; i++) {
-      listItemOrms[i] = {"orm_namespace": orms[i].orm_namespace, "orm_type": orms[i].orm_type + "ListItem"};
-    }
-
-    if (listItemOrms.length > 0) {
-      XT.Discovery.getORMSchemas(listItemOrms, schemas);
-    }
-
     /* Sanitize the JSON-Schema. */
     XT.Discovery.sanitize(schemas);
 
@@ -254,58 +247,74 @@ select xt.install_js('XT','Discovery','xtuple', $$
       XT.Discovery.getServicesSchema(null, schemas);
     }
 
+    if (!schemas) {
+      return false;
+    }
+
+    /* Get parent ListItem ORMs */
+    if (orms && orms instanceof Array && orms.length) {
+      for (var i = 0; i < orms.length; i++) {
+        listItemOrms[i] = {"orm_namespace": orms[i].orm_namespace, "orm_type": orms[i].orm_type + "ListItem"};
+      }
+    }
+
+    if (listItemOrms.length > 0) {
+      XT.Discovery.getORMSchemas(listItemOrms, schemas);
+    }
+
     /* Sort schema properties alphabetically. */
     discovery.schemas = XT.Discovery.sortObject(schemas);
 
-
     /*
      * Resources section.
      */
     discovery.resources = XT.Discovery.getResources(orm, rootUrl);
 
     /* Loop through resources and add JSON-Schema primKeyProp for methods that need it. */
-    for (var i = 0; i < orms.length; i++) {
-      var ormType = orms[i].orm_type,
-          ormNamespace = orms[i].orm_namespace,
-          thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
-          key = XT.Discovery.getKeyProps(discovery.schemas[ormType]);
-
-      if (!key) {
-        /* This should never happen. */
-        plv8.elog(ERROR, "No key found for ormType: ", ormType);
-      }
+    if (orms && orms instanceof Array && orms.length) {
+      for (var i = 0; i < orms.length; i++) {
+        var ormType = orms[i].orm_type,
+            ormNamespace = orms[i].orm_namespace,
+            thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
+            key = XT.Discovery.getKeyProps(discovery.schemas[ormType]);
+
+        if (!key) {
+          /* This should never happen. */
+          plv8.elog(ERROR, "No key found for ormType: ", ormType);
+        }
 
 
-      if (thisOrm.privileges.all.delete) {
-        discovery.resources[ormType].methods.delete.path = discovery.resources[ormType].methods.delete.path + key.name + "}";
-        discovery.resources[ormType].methods.delete.parameters = {};
-        discovery.resources[ormType].methods.delete.parameters[key.name] = key.props;
-        discovery.resources[ormType].methods.delete.parameters[key.name].location = 'path';
-        discovery.resources[ormType].methods.delete.parameterOrder = [key.name];
-      }
+        if (thisOrm.privileges.all.delete) {
+          discovery.resources[ormType].methods.delete.path = discovery.resources[ormType].methods.delete.path + key.name + "}";
+          discovery.resources[ormType].methods.delete.parameters = {};
+          discovery.resources[ormType].methods.delete.parameters[key.name] = key.props;
+          discovery.resources[ormType].methods.delete.parameters[key.name].location = 'path';
+          discovery.resources[ormType].methods.delete.parameterOrder = [key.name];
+        }
 
-      if (thisOrm.privileges.all.read) {
-        discovery.resources[ormType].methods.get.path = discovery.resources[ormType].methods.get.path + key.name + "}";
-        discovery.resources[ormType].methods.get.parameters = {};
-        discovery.resources[ormType].methods.get.parameters[key.name] = key.props;
-        discovery.resources[ormType].methods.get.parameters[key.name].location = 'path';
-        discovery.resources[ormType].methods.get.parameterOrder = [key.name];
-      }
+        if (thisOrm.privileges.all.read) {
+          discovery.resources[ormType].methods.get.path = discovery.resources[ormType].methods.get.path + key.name + "}";
+          discovery.resources[ormType].methods.get.parameters = {};
+          discovery.resources[ormType].methods.get.parameters[key.name] = key.props;
+          discovery.resources[ormType].methods.get.parameters[key.name].location = 'path';
+          discovery.resources[ormType].methods.get.parameterOrder = [key.name];
+        }
 
-      if (thisOrm.privileges.all.read) {
-        discovery.resources[ormType].methods.head.path = discovery.resources[ormType].methods.head.path + key.name + "}";
-        discovery.resources[ormType].methods.head.parameters = {};
-        discovery.resources[ormType].methods.head.parameters[key.name] = key.props;
-        discovery.resources[ormType].methods.head.parameters[key.name].location = 'path';
-        discovery.resources[ormType].methods.head.parameterOrder = [key.name];
-      }
+        if (thisOrm.privileges.all.read) {
+          discovery.resources[ormType].methods.head.path = discovery.resources[ormType].methods.head.path + key.name + "}";
+          discovery.resources[ormType].methods.head.parameters = {};
+          discovery.resources[ormType].methods.head.parameters[key.name] = key.props;
+          discovery.resources[ormType].methods.head.parameters[key.name].location = 'path';
+          discovery.resources[ormType].methods.head.parameterOrder = [key.name];
+        }
 
-      if (thisOrm.privileges.all.update) {
-        discovery.resources[ormType].methods.patch.path = discovery.resources[ormType].methods.patch.path + key.name + "}";
-        discovery.resources[ormType].methods.patch.parameters = {};
-        discovery.resources[ormType].methods.patch.parameters[key.name] = key.props;
-        discovery.resources[ormType].methods.patch.parameters[key.name].location = 'path';
-        discovery.resources[ormType].methods.patch.parameterOrder = [key.name];
+        if (thisOrm.privileges.all.update) {
+          discovery.resources[ormType].methods.patch.path = discovery.resources[ormType].methods.patch.path + key.name + "}";
+          discovery.resources[ormType].methods.patch.parameters = {};
+          discovery.resources[ormType].methods.patch.parameters[key.name] = key.props;
+          discovery.resources[ormType].methods.patch.parameters[key.name].location = 'path';
+          discovery.resources[ormType].methods.patch.parameterOrder = [key.name];
+        }
       }
     }
 
@@ -346,7 +355,7 @@ select xt.install_js('XT','Discovery','xtuple', $$
     var auth = {},
       gotOrms,
       org = plv8.execute("select current_database()"),
-      orms = [];
+      orms;
 
     rootUrl = rootUrl || "{rootUrl}";
 
@@ -356,7 +365,12 @@ select xt.install_js('XT','Discovery','xtuple', $$
       /* Build up ORMs from array. */
       for (var i = 0; i < orm.length; i++) {
         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
-        orms = orms.concat(gotOrms).unique();
+        if (gotOrms) {
+          if (!(orms instanceof Array)) {
+            orms = [];
+          }
+          orms = orms.concat(gotOrms).unique();
+        }
       }
     } else {
       orms = XT.Discovery.getIsRestORMs();
@@ -436,7 +450,12 @@ select xt.install_js('XT','Discovery','xtuple', $$
       /* Build up ORMs from array. */
       for (var i = 0; i < orm.length; i++) {
         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
-        orms = orms.concat(gotOrms).unique();
+        if (gotOrms) {
+          if (!(orms instanceof Array)) {
+            orms = [];
+          }
+          orms = orms.concat(gotOrms).unique();
+        }
       }
     } else {
       orms = XT.Discovery.getIsRestORMs();
@@ -734,9 +753,11 @@ select xt.install_js('XT','Discovery','xtuple', $$
    */
   XT.Discovery.getServicesSchema = function (orm, schemas) {
     "use strict";
+
     schemas = schemas || {};
 
-    var dispatchableObjects = XT.Discovery.getDispatchableObjects(orm),
+    var dispatchableObjects = [],
+      gotOrms,
       i,
       businessObject,
       businessObjectName,
@@ -744,6 +765,18 @@ select xt.install_js('XT','Discovery','xtuple', $$
       methodName,
       objectServices;
 
+    if (orm && typeof orm === 'string') {
+      dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
+    } else if (orm instanceof Array && orm.length) {
+      /* Build up ORMs from array. */
+      for (var i = 0; i < orm.length; i++) {
+        gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
+        dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
+      }
+    } else {
+      dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
+    }
+
     for (i = 0; i < dispatchableObjects.length; i++) {
       businessObjectName = dispatchableObjects[i];
       businessObject = XM[businessObjectName];
@@ -865,6 +898,67 @@ select xt.install_js('XT','Discovery','xtuple', $$
     return allServices;
   };
 
+  /**
+   * Return an API Discovery document's Services JSON-Schema.
+   *
+   * @param {String} Optional. An orm_type name like "Contact".
+   * @param {Object} Optional. A schema object to add schemas too.
+   * @returns {Object}
+   */
+  XT.Discovery.getServicesAuth = function (orm, auth, rootUrl) {
+    "use strict";
+
+    auth = auth || {oauth2: {scopes: {}}};
+    rootUrl = rootUrl || "{rootUrl}";
+
+    var dispatchableObjects = [],
+      gotOrms,
+      org = plv8.execute("select current_database()"),
+      i,
+      businessObject,
+      businessObjectName,
+      method,
+      methodName,
+      objectServices;
+
+    if (org.length !== 1) {
+      return false;
+    } else {
+      org = org[0].current_database;
+    }
+
+    if (orm && typeof orm === 'string') {
+      dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
+    } else if (orm instanceof Array && orm.length) {
+      /* Build up ORMs from array. */
+      for (var i = 0; i < orm.length; i++) {
+        gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
+        dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
+      }
+    } else {
+      dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
+    }
+
+    for (i = 0; i < dispatchableObjects.length; i++) {
+      businessObjectName = dispatchableObjects[i];
+      businessObject = XM[businessObjectName];
+      objectServices = {};
+      for (methodName in businessObject) {
+        method = businessObject[methodName];
+        /*
+        Report only on documented dispatch methods. We document the methods by
+        tacking description and params attributes onto the function.
+        */
+        if (typeof method === 'function' && method.description && method.scope) {
+          auth.oauth2.scopes[rootUrl + org + "/auth/" + method.scope.camelToHyphen()] = {
+            description: "Use " + method.scope + " services"
+          }
+        }
+      }
+    }
+
+    return auth;
+  };
 
   /*
    * Helper function to convert date to string in yyyyMMdd format.
@@ -1079,7 +1173,7 @@ select xt.install_js('XT','Discovery','xtuple', $$
 
     schemas = schemas || {};
 
-    if (!orms.length) {
+    if (!orms || (orms instanceof Array && !orms.length)) {
       return false;
     }
 
diff --git a/lib/orm/source/xt/operators/any_uuid.sql b/lib/orm/source/xt/operators/any_uuid.sql
new file mode 100644 (file)
index 0000000..a4447f7
--- /dev/null
@@ -0,0 +1,11 @@
+drop operator if exists <@ (
+  uuid,
+  uuid[]
+);
+
+create operator <@ (
+  leftarg = uuid,
+  rightarg = uuid[],
+  procedure = xt.any_uuid,
+  hashes, merges
+);
index 9cd8dc0..0816e00 100644 (file)
@@ -57,6 +57,8 @@ noarg:true, regexp:true, undef:true, strict:true, trailing:true, white:true */
       value: {
         '(?)attributes': {
           '(+)': _.or(
+            { ANY:           _.isDefined },
+            { NOT_ANY:       _.isDefined },
             { EQUALS:        _.isDefined },
             { NOT_EQUALS:    _.isDefined },
             { MATCHES:       _.isString },
@@ -85,6 +87,8 @@ noarg:true, regexp:true, undef:true, strict:true, trailing:true, white:true */
      */
     operators: {
       value: {
+        ANY:          'ANY',
+        NOT_ANY:      'NOT ANY',
         EQUALS:       '=',
         NOT_EQUALS:   '!=',
         LESS_THAN:    '<',
diff --git a/test/database/joins.js b/test/database/joins.js
new file mode 100644 (file)
index 0000000..2cadd88
--- /dev/null
@@ -0,0 +1,291 @@
+/*jshint trailing:true, white:true, indent:2, strict:true, curly:true,
+  immed:true, eqeqeq:true, forin:true, latedef:true,
+  newcap:true, noarg:true, undef:true */
+/*global XT:true, describe:true, it:true, require:true, __dirname:true, before:true */
+
+var _ = require("underscore"),
+  assert = require('chai').assert,
+  datasource = require('../../node-datasource/lib/ext/datasource').dataSource,
+  path = require('path');
+
+(function () {
+  "use strict";
+  describe('The database', function () {
+    this.timeout(10 * 1000);
+
+    var loginData = require(path.join(__dirname, "../lib/login_data.js")).data,
+      datasource = require('../../../xtuple/node-datasource/lib/ext/datasource').dataSource,
+      config = require(path.join(__dirname, "../../node-datasource/config.js")),
+      creds = config.databaseServer,
+      databaseName = loginData.org,
+      isCommercial = false; // this is awkward #refactor
+
+
+    before(function (done) {
+      var sql = "select metric_value from public.metric where metric_name = 'MultiWhs';";
+      creds.database = databaseName;
+      datasource.query(sql, creds, function (err, res) {
+        isCommercial = res.rows[0].metric_value === 't';
+        done();
+      });
+
+    });
+
+    // these tests are pretty fragile to the exact numbers in the database, but have been invaluable to
+    // make sure I'm not breaking anything in the fetch refactor
+    it('should execute a query with a join filter', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 5);
+        done();
+      });
+    });
+
+    it('should execute a query with an array', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ActivityListItem","query":{"orderBy":[{"attribute":"dueDate"},{"attribute":"name"},{"attribute":"uuid"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"activityType","operator":"ANY","value":["Incident","Opportunity","ToDo","SalesOrder","SalesOrderWorkflow","PurchaseOrder","PurchaseOrderWorkflow","Project","ProjectTask","ProjectWorkflow"]}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, isCommercial ? 25 : 20);
+        done();
+      });
+    });
+
+    it('should execute a query with an array with a path', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"owner.username","operator":"ANY","isCharacteristic":false,"value":["admin","foo"]}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 5);
+        done();
+      });
+    });
+
+    it('should execute a query with a simple filter', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, isCommercial ? 30 : 29);
+        done();
+      });
+    });
+
+    it('should execute a query with a simple filter and a join filter', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 5);
+        done();
+      });
+    });
+
+    it('should execute a query with a simple filter and two join filters', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"account.number","operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.length, 0);
+        done();
+      });
+    });
+
+    it('should execute a query with an array of attributes', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"},{"attribute":"firstName"},{"attribute":"primaryEmail"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["number","name","firstName","lastName","jobTitle","phone","alternate","fax","primaryEmail","webAddress","accountParent"],"operator":"MATCHES","value":"coltraine"},{"attribute":"isActive","operator":"=","value":true}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 1);
+        done();
+      });
+    });
+
+    it('should execute a query with two join filters on the same table', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"updated","descending":true},{"attribute":"number","descending":true,"numeric":true}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, isCommercial ? 7 : 4);
+        done();
+      });
+    });
+
+    it('should execute a query with ambiguous column filters', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ToDoListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"dueDate"},{"attribute":"name"}],"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"},{"attribute":"uuid","operator":"=","value":"23eef809-2f7c-4289-9eab-a72d621a6adb"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        done();
+      });
+    });
+
+    it('should execute a query filtering on an orm-extended field', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ProspectRelation","query":{"orderBy":[{"attribute":"number"}],"parameters":[{"attribute":"isActive","operator":"=","value":true}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 1);
+        done();
+      });
+    });
+
+    it('should execute an item-site fetch', function (done) {
+      var sql = 'select xt.js_init(true);select xt.post($${"nameSpace":"XM","type":"ItemSiteRelation","dispatch":{"functionName":"fetch","parameters":{"parameters":[{"attribute":"item.number","value":"BTRUCK1"},{"attribute":"site.code","value":"WH1"}]}},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].post);
+        assert.equal(results.length, 1);
+        done();
+      });
+    });
+
+    it('should execute a complicated item-site fetch', function (done) {
+      var sql = 'select xt.js_init(true);select xt.post($${"nameSpace":"XM","type":"ItemSiteRelation","dispatch":{"functionName":"fetch","parameters":{"parameters":[{"attribute":"item.isSold","value":true},{"attribute":"item.isActive","value":true},{"attribute":"isSold","value":true},{"attribute":"isActive","value":true},{"attribute":"site.code","value":"WH1"},{"attribute":"customer","value":"TTOYS"},{"attribute":["number","barcode"],"operator":"BEGINS_WITH","value":"bt","keySearch":true}],"orderBy":[{"attribute":"number"},{"attribute":"barcode"}],"rowLimit":1}},"username":"admin","encryptionKey":"this is any content"}$$)';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].post);
+        assert.equal(results.length, 1);
+        done();
+      });
+    });
+
+    it('should supported a nested order-by', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ItemSource","query":{"orderBy":[{"attribute":"vendorItemNumber"},{"attribute":"vendor.name"}],"parameters":[{"attribute":"isActive","value":true},{"attribute":"effective","operator":"<=","value":"2014-03-20T04:00:00.000Z"},{"attribute":"expires","operator":">=","value":"2014-03-22T01:18:09.202Z"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, isCommercial ? 21 : 20);
+        done();
+      });
+    });
+
+    it('should supported an ambiguous primary key', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IssueToShipping","query":{"orderBy":[{"attribute":"lineNumber"},{"attribute":"subNumber"}],"parameters":[{"attribute":"order.uuid","operator":"=","value":"d3538bbd-826a-4351-b35c-795d7db99ba0"}],"rowOffset":0,"rowLimit":50},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      if (!isCommercial) {
+        // forget about it
+        done();
+        return;
+      }
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 1);
+        done();
+      });
+    });
+
+    it('should allow the shortcut of querying a toOne directly by its natural key', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"orderBy":[{"attribute":"priorityOrder"},{"attribute":"updated","descending":true},{"attribute":"number","descending":true,"numeric":true}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"category","operator":"","isCharacteristic":false,"value":"Customer"},{"attribute":["owner.username","assignedTo.username"],"operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 1);
+        done();
+      });
+    });
+
+    it('should allow the shortcut of querying a toOne directly by its natural key with an attribute array', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"SalesOrderListItem","query":{"orderBy":[{"attribute":"number"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":["number","customerPurchaseOrderNumber","status","orderNotes","currency","billtoName","billtoCity","billtoState","billtoCountry","shiptoName","shiptoCity","shiptoState","shiptoCountry"],"operator":"MATCHES","value":"trem"},{"attribute":"status","value":"O"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 1);
+        done();
+      });
+    });
+
+    it('should allow querying by characteristics', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"orderBy":[{"attribute":"lastName"},{"attribute":"firstName"},{"attribute":"primaryEmail"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"CONTACT-BIRTHDAY","operator":"MATCHES","isCharacteristic":true,"value":"foo"}]},"username":"admin","encryptionKey":"this is any content"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.length, 0);
+        done();
+      });
+    });
+
+    it('should work with an empty parameters list', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"IncidentListItem","query":{"parameters":[],"orderBy":[],"rowLimit":100},"username":"admin","encryptionKey":"xq5j2"}$$);';
+
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data.length, 4);
+        done();
+      });
+    });
+
+    it('should facilitate the count query', function (done) {
+      var sql = 'select xt.js_init(true);select xt.get($${"nameSpace":"XM","type":"ContactListItem","query":{"count":true,"orderBy":[{"attribute":"lastName"}],"rowOffset":0,"rowLimit":50,"parameters":[{"attribute":"isActive","operator":"=","value":true},{"attribute":"owner.username","operator":"","isCharacteristic":false,"value":"admin"}]},"username":"admin","encryptionKey":"foo"}$$);';
+      datasource.query(sql, creds, function (err, res) {
+        var results;
+        assert.isNull(err);
+        assert.equal(1, res.rowCount, JSON.stringify(res.rows));
+        results = JSON.parse(res.rows[1].get);
+        assert.equal(results.data[0].count, 5);
+        done();
+      });
+    });
+
+
+  });
+}());
+
+