Repository: dynamoosejs/dynamoose
Pull request #735 (2020-03-19 23:49:43)

Bug Fixes


Patch:
diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
index c59b8237..c950b229 100644
--- a/BREAKING_CHANGES.md
+++ b/BREAKING_CHANGES.md
@@ -2,14 +2,16 @@
 
 ## 2.0 - incomplete list
 
-- Everything in Dynamoose are now classes. This includes `Model` and `Schema`. This means to initalize a new instance of one of these items, you must use the `new` keyword before it
+- Everything in Dynamoose are now classes. This includes `Model` and `Schema`. This means to initialize a new instance of one of these items, you must use the `new` keyword before it
 - `scan.count()` has been removed, and `scan.counts()` has been renamed to `scan.count()`.
 - Schema `default` value does not pass the model instance into `default` functions any more.
 - `Model.update`
 	- `$LISTAPPEND` has been removed, and `$ADD` now includes the behavior of `$LISTAPPEND`
 	- `$DELETE` now maps to the correct underlying DynamoDB method instead of the previous behavior of mapping to `$REMOVE`
+	- `$PUT` has been replaced with `$SET`
 - `dynamoose.model` has been renamed to `dynamoose.Model`
 - `dynamoose.local` has been renamed to `dynamoose.aws.ddb.local`
+- `dynamoose.setDDB` has been renamed to `dynamoose.aws.ddb.set`
 - `Model.getTableReq` has been renamed to `Model.table.create.request`
 - `Model.table.create.request` (formerly `Model.getTableReq`) is now an async function
 - `model.originalItem` has been renamed to `model.original` (or `Document.original`)
@@ -21,3 +23,8 @@
 - `Model.transaction.conditionCheck` has been renamed to `Model.transaction.condition`
 - `Model.transaction.condition` options parameter now gets appended to the object returned. This means you can no longer use the helpers that Dynamoose provided to make conditions. Instead, pass in the DynamoDB API level conditions you wish to use
 - In the past the `saveUnknown` option for attribute names would handle all nested properties. Now you must use `*` to indicate one level of wildcard or `**` to indicate infinate levels of wildcard. So if you have an object property (`address`) and want to parse one level of values (no sub objects) you can use `address.*`, or `address.**` to all for infinate levels of values (including sub objects)
+- `useNativeBooleans` & `useDocumentTypes` have been removed from the Model settings
+- `Map` attribute type has been replaced with `Object`
+- `List` attribute type has been replaced with `Array`
+- `Scan.null` & `Query.null` have been removed
+- DynamoDB set types are now returned as JavaScript Set's instead of Array's
diff --git a/docs/api/Query.md b/docs/api/Query.md
index bab70693..dfee2478 100644
--- a/docs/api/Query.md
+++ b/docs/api/Query.md
@@ -98,22 +98,12 @@ Cat.query().where("id"); // Currently this query has no behavior and will query
 Cat.query().where("id").eq(1); // Since this query has a comparison function (eq) after the conditional it will complete the conditional and only query items where `id` = 1
 ```
 
-## query.null()
-
-This comparison function will check to see if the given filter key is null.
-
-```js
-Cat.query().filter("name").null(); // Return all items where `name` is null
-```
-
 ## query.eq(value)
 
-This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter. If `null`, `undefined`, or an empty string is passed as the value parameter, this function will behave just like [`query.null()`](#querynull).
+This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter.
 
 ```js
 Cat.query().filter("name").eq("Tom"); // Return all items where `name` equals `Tom`
-
-Cat.query().filter("name").eq(); // Same as `Cat.query().filter("name").null()`
 ```
 
 ## query.lt(value)
diff --git a/docs/api/Scan.md b/docs/api/Scan.md
index 7760e6b2..02a514be 100644
--- a/docs/api/Scan.md
+++ b/docs/api/Scan.md
@@ -91,22 +91,12 @@ Cat.scan().filter("id").eq(1); // Since this scan has a comparison function (eq)
 
 This function is identical to [`scan.filter(key)`](#scanfilterkey) and just used as an alias.
 
-## scan.null()
-
-This comparison function will check to see if the given filter key is null.
-
-```js
-Cat.scan().filter("name").null(); // Return all items where `name` is null
-```
-
 ## scan.eq(value)
 
-This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter. If `null`, `undefined`, or an empty string is passed as the value parameter, this function will behave just like [`scan.null()`](#scannull).
+This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter.
 
 ```js
 Cat.scan().filter("name").eq("Tom"); // Return all items where `name` equals `Tom`
-
-Cat.scan().filter("name").eq(); // Same as `Cat.scan().filter("name").null()`
 ```
 
 ## scan.lt(value)
diff --git a/lib/Document.js b/lib/Document.js
index 03908c51..b2b42067 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -112,18 +112,13 @@ function DocumentCarrier(model) {
 		function mainCheck(finalObjectPre, key, value) {
 			const finalObject = {...finalObjectPre};
 			const genericKey = key.replace(/\d+/gu, "0"); // This is a key replacing all list numbers with 0 to standardize things like checking if it exists in the schema
-			const keyParts = key.split(".");
-			const parentKey = [...keyParts].splice(0, keyParts.length - 1).join(".");
-			let parentKeyIsArray;
-			try {
-				parentKeyIsArray = parentKey && model.schema.getAttributeType(parentKey) === "L";
-			} catch (e) {} // eslint-disable-line no-empty
-			const existsInSchema = model.schema.attributes().includes(genericKey) || parentKeyIsArray;
-			const isCustomType = existsInSchema && model.schema.getAttributeTypeDetails(key).customType;
+			const existsInSchema = model.schema.attributes().includes(genericKey);
+			const typeDetails = existsInSchema && model.schema.getAttributeTypeDetails(key);
+			const isCustomType = existsInSchema && typeDetails.customType;
 			const valueType = typeof value;
-			const customValue = isCustomType ? model.schema.getAttributeTypeDetails(key).customType.functions[settings.type](value) : null;
+			const customValue = isCustomType ? typeDetails.customType.functions[settings.type](value) : null;
 			const customValueType = isCustomType ? typeof customValue : null;
-			let attributeType = existsInSchema || parentKeyIsArray ? utils.attribute_types.find((type) => type.dynamodbType === model.schema.getAttributeType(key)) : null;
+			let attributeType = existsInSchema ? utils.attribute_types.find((type) => type.dynamodbType === model.schema.getAttributeType(key)) : null;
 			const expectedType = existsInSchema ? attributeType.javascriptType : null;
 			let typeMatches = existsInSchema ? valueType === expectedType || (attributeType.isOfType && attributeType.isOfType(value, settings.type)) : null;
 			if (existsInSchema && attributeType.isOfType && attributeType.isOfType(isCustomType && settings.type === "toDynamo" ? customValue : value, settings.type)) {
diff --git a/lib/DocumentRetriever.js b/lib/DocumentRetriever.js
index aadcf156..56fe353f 100644
--- a/lib/DocumentRetriever.js
+++ b/lib/DocumentRetriever.js
@@ -37,31 +37,92 @@ function main(documentRetrieverTypeString) {
 					});
 				} else if (object) {
 					this.settings.pending.key = object;
+					if (documentRetrieverType.type === "query") {
+						this.settings.pending.queryCondition = "hash";
+					}
 				}
 
 				return this;
 			}
 		};
-		C.prototype[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`] = function() {
+		C.prototype[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`] = async function() {
 			const object = {
-				"TableName": model.name,
-				[`${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`]: {}
+				"TableName": model.name
 			};
 			if (documentRetrieverType.type === "query") {
-				object.KeyConditions = {};
+				object.KeyConditionExpression = "#qha = :qhv";
 			}
 
-			Object.keys(this.settings.filters).forEach((key) => {
+			Object.keys(this.settings.filters).forEach((key, index) => {
+				if (!object.ExpressionAttributeNames || !object.ExpressionAttributeValues) {
+					object.ExpressionAttributeNames = {};
+					object.ExpressionAttributeValues = {};
+				}
+
 				const filter = this.settings.filters[key];
-				let value = filter.value;
-				if (!Array.isArray(value)) {
-					value = [value];
+				const value = filter.value;
+				// if (!Array.isArray(value)) {
+				// 	value = [value];
+				// }
+				// value = value.map((item) => aws.converter().input(item));
+				// object[filter.queryCondition ? "KeyConditions" : `${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`][key] = {
+				// 	"ComparisonOperator": filter.type,
+				// 	"AttributeValueList": value
+				// };
+				let keys = {"name": `#a${index}`, "value": `:v${index}`};
+				if (filter.queryCondition === "hash") {
+					keys = {"name": "#qha", "value": ":qhv"};
+				} else if (filter.queryCondition === "range") {
+					keys = {"name": "#qra", "value": ":qrv"};
+				}
+				object.ExpressionAttributeNames[keys.name] = key;
+				object.ExpressionAttributeValues[keys.value] = aws.converter().input(value);
+
+				if (!filter.queryCondition) {
+					if (!object.FilterExpression) {
+						object.FilterExpression = "";
+					}
+					if (object.FilterExpression !== "") {
+						object.FilterExpression = `${object.FilterExpression} AND `;
+					}
+
+					let expression = "";
+					switch (filter.type) {
+					case "EQ":
+					case "NE":
+						expression = `${keys.name} ${filter.type === "EQ" ? "=" : "<>"} ${keys.value}`;
+						break;
+					case "IN":
+						delete object.ExpressionAttributeValues[keys.value];
+						expression = `${keys.name} IN (${value.map((v, i) => `${keys.value}-${i + 1}`).join(", ")})`;
+						value.forEach((valueItem, i) => {
+							object.ExpressionAttributeValues[`${keys.value}-${i + 1}`] = aws.converter().input(valueItem);
+						});
+						break;
+					case "GT":
+					case "GE":
+					case "LT":
+					case "LE":
+						expression = `${keys.name} ${filter.type.startsWith("G") ? ">" : "<"}${filter.type.endsWith("E") ? "=" : ""} ${keys.value}`;
+						break;
+					case "BETWEEN":
+						expression = `${keys.name} BETWEEN ${keys.value}-1 AND ${keys.value}-2`;
+						object.ExpressionAttributeValues[`${keys.value}-1`] = aws.converter().input(value[0]);
+						object.ExpressionAttributeValues[`${keys.value}-2`] = aws.converter().input(value[1]);
+						delete object.ExpressionAttributeValues[keys.value];
+						break;
+					case "CONTAINS":
+					case "NOT_CONTAINS":
+						expression = `${filter.type === "NOT_CONTAINS" ? "NOT " : ""}contains (${keys.name}, ${keys.value})`;
+						break;
+					case "BEGINS_WITH":
+						expression = `begins_with (${keys.name}, ${keys.value})`;
+						break;
+					}
+					object.FilterExpression = `${object.FilterExpression}${expression}`;
+				} else if (filter.queryCondition === "range") {
+					object.KeyConditionExpression = `${object.KeyConditionExpression} AND ${keys.name} = ${keys.value}`;
 				}
-				value = value.map((item) => aws.converter().input(item));
-				object[filter.queryCondition ? "KeyConditions" : `${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`][key] = {
-					"ComparisonOperator": filter.type,
-					"AttributeValueList": value
-				};
 			});
 			if (this.settings.limit) {
 				object.Limit = this.settings.limit;
@@ -74,6 +135,12 @@ function main(documentRetrieverTypeString) {
 			}
 			if (this.settings.index) {
 				object.IndexName = this.settings.index;
+			} else if (documentRetrieverType.type === "query") {
+				const indexes = await model.schema.getIndexes(model);
+				// TODO change `Array.prototype.concat.apply` to be a custom flatten function
+				const preferredIndexes = Array.prototype.concat.apply([], Object.values(indexes)).filter((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qha"] && key.KeyType === "HASH")));
+				const index = !object.ExpressionAttributeNames["#qra"] ? preferredIndexes[0] : preferredIndexes.find((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qra"] && key.KeyType === "RANGE")));
+				object.IndexName = index.IndexName;
 			}
 			if (this.settings.consistent) {
 				object.ConsistentRead = this.settings.consistent;
@@ -95,7 +162,6 @@ function main(documentRetrieverTypeString) {
 				"LE": "GT",
 				"LT": "GE",
 				"BETWEEN": null,
-				"NULL": "NOT_NULL",
 				"CONTAINS": "NOT_CONTAINS",
 				"BEGINS_WITH": null
 			};
@@ -126,7 +192,7 @@ function main(documentRetrieverTypeString) {
 			};
 
 			if (pending.queryCondition) {
-				instance.settings.filters[pending.key].queryCondition = true;
+				instance.settings.filters[pending.key].queryCondition = pending.queryCondition;
 			}
 
 			instance.settings.pending = {};
@@ -151,9 +217,8 @@ function main(documentRetrieverTypeString) {
 				array[`times${utils.capitalize_first_letter(documentRetrieverType.pastTense)}`] = timesRequested;
 				return array;
 			};
-			const promise = model.pendingTaskPromise().then(() => {
+			const promise = model.pendingTaskPromise().then(() => this[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`]()).then((request) => {
 				const ddb = aws.ddb();
-				const request = this[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`]();
 
 				const allRequest = (extraParameters = {}) => {
 					let promise = ddb[documentRetrieverType.type]({...request, ...extraParameters}).promise();
@@ -219,13 +284,12 @@ function main(documentRetrieverTypeString) {
 			C.prototype.where = C.prototype.filter;
 		} else {
 			C.prototype.where = function(key) {
-				this.settings.pending = {key, "queryCondition": true};
+				this.settings.pending = {key, "queryCondition": "range"};
 				return this;
 			};
 		}
 		const filterTypes = [
-			{"name": "null", "typeName": "NULL", "value": []},
-			{"name": "eq", "typeName": "EQ", "default": {"typeName": "null"}},
+			{"name": "eq", "typeName": "EQ"},
 			{"name": "lt", "typeName": "LT"},
 			{"name": "le", "typeName": "LE"},
 			{"name": "gt", "typeName": "GT"},
@@ -237,8 +301,8 @@ function main(documentRetrieverTypeString) {
 		];
 		filterTypes.forEach((item) => {
 			C.prototype[item.name] = function(value) {
-				if (!value && item.default) {
-					return this[item.default.typeName](value);
+				if (this.settings.pending.queryCondition && item.name !== "eq") {
+					throw new Error.InvalidParameter("Equals must follow range or hash key when querying data");
 				}
 
 				this.settings.pending.value = item.value || (item.multipleArguments ? [...arguments] : value);
diff --git a/lib/Model.js b/lib/Model.js
index 6c9986ca..71ffb3c8 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -132,8 +132,8 @@ async function createTable(model) {
 async function createTableRequest(model) {
 	return {
 		"TableName": model.name,
-		...getProvisionedThroughput(model),
-		...await model.schema.getCreateTableAttributeParams()
+		...utils.dynamoose.get_provisioned_throughput(model.options),
+		...await model.schema.getCreateTableAttributeParams(model)
 	};
 }
 function updateTimeToLive(model) {
@@ -145,20 +145,6 @@ function updateTimeToLive(model) {
 		}
 	});
 }
-function getProvisionedThroughput(model) {
-	if (model.options.throughput === "ON_DEMAND") {
-		return {
-			"BillingMode": "PAY_PER_REQUEST"
-		};
-	} else {
-		return {
-			"ProvisionedThroughput": {
-				"ReadCapacityUnits": typeof model.options.throughput === "number" ? model.options.throughput : model.options.throughput.read,
-				"WriteCapacityUnits": typeof model.options.throughput === "number" ? model.options.throughput : model.options.throughput.write
-			}
-		};
-	}
-}
 function waitForActive(model) {
 	return () => new Promise((resolve, reject) => {
 		const start = Date.now();
@@ -203,8 +189,9 @@ async function getTableDetails(model, settings = {}) {
 }
 async function updateTable(model) {
 	const currentThroughput = (await getTableDetails(model)).Table;
-	const expectedThroughput = getProvisionedThroughput(model);
+	const expectedThroughput = utils.dynamoose.get_provisioned_throughput(model.options);
 	if ((expectedThroughput.BillingMode === currentThroughput.BillingMode && expectedThroughput.BillingMode) || ((currentThroughput.ProvisionedThroughput || {}).ReadCapacityUnits === (expectedThroughput.ProvisionedThroughput || {}).ReadCapacityUnits && currentThroughput.ProvisionedThroughput.WriteCapacityUnits === expectedThroughput.ProvisionedThroughput.WriteCapacityUnits)) {
+	// if ((expectedThroughput.BillingMode === currentThroughput.BillingModeSummary.BillingMode && expectedThroughput.BillingMode) || ((currentThroughput.ProvisionedThroughput || {}).ReadCapacityUnits === (expectedThroughput.ProvisionedThroughput || {}).ReadCapacityUnits && currentThroughput.ProvisionedThroughput.WriteCapacityUnits === expectedThroughput.ProvisionedThroughput.WriteCapacityUnits)) {
 		return {"promise": () => Promise.resolve()};
 	}
 
diff --git a/lib/Schema.js b/lib/Schema.js
index e3f8175d..45683fe6 100644
--- a/lib/Schema.js
+++ b/lib/Schema.js
@@ -88,14 +88,18 @@ Schema.prototype.defaultCheck = async function(key, value, settings) {
 Schema.prototype.getIndexAttributes = async function() {
 	return (await Promise.all(this.attributes().map(async (attribute) => ({"index": await this.getAttributeSettingValue("index", attribute), attribute})))).filter((obj) => obj.index);
 };
-Schema.prototype.getIndexes = async function() {
+Schema.prototype.getIndexRangeKeyAttributes = async function() {
+	const indexes = await this.getIndexAttributes();
+	return indexes.map((index) => index.index.rangeKey).filter((a) => Boolean(a)).map((a) => ({"attribute": a}));
+};
+Schema.prototype.getIndexes = async function(model) {
 	return (await this.getIndexAttributes()).reduce((accumulator, currentValue) => {
 		let indexValue = currentValue.index;
 		const attributeValue = currentValue.attribute;
 
 		const dynamoIndexObject = {
 			"IndexName": indexValue.name || `${attributeValue}${indexValue.global ? "GlobalIndex" : "LocalIndex"}`,
-			"KeySchema": [{"AttributeName": attributeValue, "KeyType": indexValue.rangeKey ? "HASH" : "RANGE"}],
+			"KeySchema": [{"AttributeName": attributeValue, "KeyType": "HASH"}],
 			"Projection": {"ProjectionType": "KEYS_ONLY"}
 		};
 		if (indexValue.project || typeof indexValue.project === "undefined" || indexValue.project === null) {
@@ -103,14 +107,12 @@ Schema.prototype.getIndexes = async function() {
 		}
 		if (indexValue.rangeKey) {
 			dynamoIndexObject.KeySchema.push({"AttributeName": indexValue.rangeKey, "KeyType": "RANGE"});
-		} else {
-			dynamoIndexObject.KeySchema.unshift({"AttributeName": this.getHashKey(), "KeyType": "HASH"});
 		}
-		if (indexValue.global && indexValue.throughput) {
-			dynamoIndexObject.ProvisionedThroughput = {
-				"ReadCapacityUnits": `${indexValue.throughput.read || indexValue.throughput}`,
-				"WriteCapacityUnits": `${indexValue.throughput.write || indexValue.throughput}`
-			};
+		if (indexValue.global) {
+			const throughputObject = utils.dynamoose.get_provisioned_throughput(indexValue.throughput ? indexValue : model.options);
+			if (throughputObject.ProvisionedThroughput) {
+				dynamoIndexObject.ProvisionedThroughput = throughputObject.ProvisionedThroughput;
+			}
 		}
 		if (!accumulator[(indexValue.global ? "GlobalSecondaryIndexes" : "LocalSecondaryIndexes")]) {
 			accumulator[(indexValue.global ? "GlobalSecondaryIndexes" : "LocalSecondaryIndexes")] = [];
@@ -260,7 +262,7 @@ Schema.prototype.getAttributeType = function(key) {
 	return this.getAttributeTypeDetails(key).dynamodbType;
 };
 
-Schema.prototype.getCreateTableAttributeParams = async function() {
+Schema.prototype.getCreateTableAttributeParams = async function(model) {
 	const hashKey = this.getHashKey();
 	const AttributeDefinitions = [
 		{
@@ -268,6 +270,7 @@ Schema.prototype.getCreateTableAttributeParams = async function() {
 			"AttributeType": this.getAttributeType(hashKey)
 		}
 	];
+	const AttributeDefinitionsNames = [hashKey];
 	const KeySchema = [
 		{
 			"AttributeName": hashKey,
@@ -281,13 +284,20 @@ Schema.prototype.getCreateTableAttributeParams = async function() {
 			"AttributeName": rangeKey,
 			"AttributeType": this.getAttributeType(rangeKey)
 		});
+		AttributeDefinitionsNames.push(rangeKey);
 		KeySchema.push({
 			"AttributeName": rangeKey,
 			"KeyType": "RANGE"
 		});
 	}
 
-	(await this.getIndexAttributes()).map((obj) => obj.attribute).forEach((index) => {
+	// TODO change `Array.prototype.concat.apply` to be a custom flatten function
+	Array.prototype.concat.apply([], (await Promise.all([this.getIndexAttributes(), this.getIndexRangeKeyAttributes()]))).map((obj) => obj.attribute).forEach((index) => {
+		if (AttributeDefinitionsNames.includes(index)) {
+			return;
+		}
+
+		AttributeDefinitionsNames.push(index);
 		AttributeDefinitions.push({
 			"AttributeName": index,
 			"AttributeType": this.getAttributeType(index)
@@ -297,7 +307,7 @@ Schema.prototype.getCreateTableAttributeParams = async function() {
 	return {
 		AttributeDefinitions,
 		KeySchema,
-		...await this.getIndexes()
+		...await this.getIndexes(model)
 	};
 };
 
diff --git a/lib/utils/dynamoose/get_provisioned_throughput.js b/lib/utils/dynamoose/get_provisioned_throughput.js
new file mode 100644
index 00000000..49935d37
--- /dev/null
+++ b/lib/utils/dynamoose/get_provisioned_throughput.js
@@ -0,0 +1,14 @@
+module.exports = (options) => {
+	if (options.throughput === "ON_DEMAND") {
+		return {
+			"BillingMode": "PAY_PER_REQUEST"
+		};
+	} else {
+		return {
+			"ProvisionedThroughput": {
+				"ReadCapacityUnits": typeof options.throughput === "number" ? options.throughput : options.throughput.read,
+				"WriteCapacityUnits": typeof options.throughput === "number" ? options.throughput : options.throughput.write
+			}
+		};
+	}
+};
diff --git a/lib/utils/dynamoose/index.js b/lib/utils/dynamoose/index.js
index fc8f4d9d..f426d66e 100644
--- a/lib/utils/dynamoose/index.js
+++ b/lib/utils/dynamoose/index.js
@@ -1,3 +1,4 @@
 module.exports = {
-	"saveunknown_attribute_allowed_check": require("./saveunknown_attribute_allowed_check")
+	"saveunknown_attribute_allowed_check": require("./saveunknown_attribute_allowed_check"),
+	"get_provisioned_throughput": require("./get_provisioned_throughput")
 };


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 15a2be3f..e2363097 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -1389,6 +1389,11 @@ describe("Document", () => {
 				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true}],
 				"model": ["User", {"id": Number}, {"create": false, "waitForActive": false, "expires": {"ttl": 1000, "attribute": "ttl", "items": {"returnExpired": false}}}],
 				"output": null
+			},
+			{
+				"input": [{"id": 1, "items": ["test"]}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number, "items": Array}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": ["test"]}
 			}
 		];
 
diff --git a/test/Query.js b/test/Query.js
index 88c6dd90..fe80f494 100644
--- a/test/Query.js
+++ b/test/Query.js
@@ -30,7 +30,7 @@ describe("Query", () => {
 
 	let Model;
 	beforeEach(() => {
-		Model = new dynamoose.Model("Cat", {"id": Number, "name": String});
+		Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true}}});
 	});
 
 	describe("Model.query", () => {
@@ -49,7 +49,7 @@ describe("Query", () => {
 		it("Should set pending key if string passed into query function", () => {
 			const id = "id";
 			const query = Model.query(id);
-			expect(query.settings.pending).to.eql({"key": id});
+			expect(query.settings.pending).to.eql({"key": id, "queryCondition": "hash"});
 		});
 
 		it("Should set filters correctly for object passed into query function", () => {
@@ -60,6 +60,14 @@ describe("Query", () => {
 		it("Should throw error if unknown comparison operator is passed in", () => {
 			expect(() => Model.query({"name": {"unknown": "Charlie"}})).to.throw("The type: unknown is invalid for the query operation.");
 		});
+
+		it("Should throw error if pending hash key isn't followed by equals", async () => {
+			expect(() => Model.query("age").gt(5)).to.throw("Equals must follow range or hash key when querying data");
+		});
+
+		it("Should throw error if pending range key isn't followed by equals", async () => {
+			expect(() => Model.query("age").eq(5).where("type").gt(5)).to.throw("Equals must follow range or hash key when querying data");
+		});
 	});
 
 	describe("query.exec", () => {
@@ -80,41 +88,41 @@ describe("Query", () => {
 			describe(callType.name, () => {
 				it("Should return correct result", async () => {
 					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie"}]);
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie"}]);
 				});
 
 				it("Should return null for expired object", async () => {
-					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "ttl": {"N": "1"}}]});
-					Model = new dynamoose.Model("Cat", {"id": Number}, {"expires": {"ttl": 1000, "items": {"returnExpired": false}}});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([]);
+					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}, "ttl": {"N": "1"}}]});
+					Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true}}}, {"expires": {"ttl": 1000, "items": {"returnExpired": false}}});
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([]);
 				});
 
 				it("Should return expired object if returnExpired is not set", async () => {
-					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "ttl": {"N": "1"}}]});
-					Model = new dynamoose.Model("Cat", {"id": Number}, {"expires": {"ttl": 1000}});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([{"id": 1, "ttl": new Date(1000)}]);
+					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}, "ttl": {"N": "1"}}]});
+					Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true}}}, {"expires": {"ttl": 1000}});
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie", "ttl": new Date(1000)}]);
 				});
 
 				it("Should return correct result if unknown properties are in DynamoDB", async () => {
 					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}, "age": {"N": "1"}}]});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie"}]);
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie"}]);
 				});
 
 				it("Should return correct result if using custom types", async () => {
-					Model = new dynamoose.Model("Cat", {"id": Number, "name": String, "birthday": Date});
+					Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true}}, "birthday": Date});
 					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}, "birthday": {"N": "1"}}]});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie", "birthday": new Date(1)}]);
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie", "birthday": new Date(1)}]);
 				});
 
 				it("Should return correct result for saveUnknown", async () => {
-					Model = new dynamoose.Model("Cat", new dynamoose.Schema({"id": Number}, {"saveUnknown": true}));
-					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]});
-					expect((await callType.func(Model.query().exec).bind(Model.query())()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie"}]);
+					Model = new dynamoose.Model("Cat", new dynamoose.Schema({"id": Number, "name": {"type": String, "index": {"global": true}}}, {"saveUnknown": true}));
+					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}, "age": {"N": 10}}]});
+					expect((await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))()).map((item) => ({...item}))).to.eql([{"id": 1, "name": "Charlie", "age": 10}]);
 				});
 
 				it("Should return correct metadata in result", async () => {
 					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}], "Count": 1, "QueriedCount": 1});
-					const result = await callType.func(Model.query().exec).bind(Model.query())();
+					const result = await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))();
 					expect(result.lastKey).to.eql(undefined);
 					expect(result.count).to.eql(1);
 					expect(result.queriedCount).to.eql(1);
@@ -123,72 +131,108 @@ describe("Query", () => {
 
 				it("Should return correct lastKey", async () => {
 					queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}], "Count": 1, "QueriedCount": 1, "LastEvaluatedKey": {"id": {"N": "5"}}});
-					const result = await callType.func(Model.query().exec).bind(Model.query())();
+					const result = await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))();
 					expect(result.lastKey).to.eql({"id": 5});
 				});
 
 				it("Should send correct request on query.exec", async () => {
 					queryPromiseResolver = () => ({"Items": []});
-					await callType.func(Model.query().exec).bind(Model.query())();
-					expect(queryParams).to.eql({"QueryFilter": {}, "TableName": "Cat", "KeyConditions": {}});
+					await callType.func(Model.query("name").eq("Charlie").exec).bind(Model.query("name").eq("Charlie"))();
+					expect(queryParams).to.eql({
+						"TableName": "Cat",
+						"IndexName": "nameGlobalIndex",
+						"ExpressionAttributeNames": {
+							"#qha": "name"
+						},
+						"ExpressionAttributeValues": {
+							":qhv": {"S": "Charlie"}
+						},
+						"KeyConditionExpression": "#qha = :qhv"
+					});
 				});
 
 				it("Should send correct request on query.exec with filters", async () => {
 					queryPromiseResolver = () => ({"Items": []});
-					const query = Model.query().filter("id").eq("test");
+					const query = Model.query("name").eq("Charlie").filter("id").eq("test");
 					await callType.func(query.exec).bind(query)();
-					expect(queryParams).to.eql({"QueryFilter": {
-						"id": {
-							"ComparisonOperator": "EQ",
-							"AttributeValueList": [
-								{"S": "test"}
-							]
-						}
-					}, "TableName": "Cat", "KeyConditions": {}});
+					expect(queryParams).to.eql({
+						"TableName": "Cat",
+						"IndexName": "nameGlobalIndex",
+						"ExpressionAttributeNames": {
+							"#qha": "name",
+							"#a1": "id"
+						},
+						"ExpressionAttributeValues": {
+							":qhv": {"S": "Charlie"},
+							":v1": {"S": "test"}
+						},
+						"FilterExpression": "#a1 = :v1",
+						"KeyConditionExpression": "#qha = :qhv"
+					});
 				});
 
-				it("Should send correct request on query.exec with filters and multiple values", async () => {
+				it("Should send correct request on query.exec with multiple filters", async () => {
 					queryPromiseResolver = () => ({"Items": []});
-					const query = Model.query().filter("id").between(1, 3);
+					Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true}}, "age": Number, "breed": String});
+					const query = Model.query("name").eq("Charlie").filter("breed").eq("Cat").and().filter("age").gt(2);
 					await callType.func(query.exec).bind(query)();
-					expect(queryParams).to.eql({"QueryFilter": {
-						"id": {
-							"ComparisonOperator": "BETWEEN",
-							"AttributeValueList": [
-								{"N": "1"},
-								{"N": "3"}
-							]
-						}
-					}, "TableName": "Cat", "KeyConditions": {}});
+					expect(queryParams).to.eql({
+						"TableName": "Cat",
+						"IndexName": "nameGlobalIndex",
+						"ExpressionAttributeNames": {
+							"#qha": "name",
+							"#a1": "breed",
+							"#a2": "age"
+						},
+						"ExpressionAttributeValues": {
+							":qhv": {"S": "Charlie"},
+							":v1": {"S": "Cat"},
+							":v2": {"N": "2"}
+						},
+						"FilterExpression": "#a1 = :v1 AND #a2 > :v2",
+						"KeyConditionExpression": "#qha = :qhv"
+					});
 				});
 
-				it("Should send correct request on query.exec with query condition", async () => {
+				it("Should send correct request on query.exec with filters and multiple values", async () => {
 					queryPromiseResolver = () => ({"Items": []});
-					const query = Model.query().where("id").eq("test");
+					const query = Model.query("name").eq("Charlie").filter("id").between(1, 3);
 					await callType.func(query.exec).bind(query)();
-					expect(queryParams).to.eql({"KeyConditions": {
-						"id": {
-							"ComparisonOperator": "EQ",
-							"AttributeValueList": [
-								{"S": "test"}
-							]
-						}
-					}, "TableName": "Cat", "QueryFilter": {}});
+					expect(queryParams).to.eql({
+						"TableName": "Cat",
+						"IndexName": "nameGlobalIndex",
+						"ExpressionAttributeNames": {
+							"#qha": "name",
+							"#a1": "id"
+						},
+						"ExpressionAttributeValues": {
+							":qhv": {"S": "Charlie"},
+							":v1-1": {"N": "1"},
+							":v1-2": {"N": "3"}
+						},
+						"FilterExpression": "#a1 BETWEEN :v1-1 AND :v1-2",
+						"KeyConditionExpression": "#qha = :qhv"
+					});
 				});
 
-				it("Should send correct request on query.exec with query condition and multiple values", async () => {
+				it("Should send correct request on query.exec with query condition", async () => {
 					queryPromiseResolver = () => ({"Items": []});
-					const query = Model.query().where("id").between(1, 3);
+					Model = new dynamoose.Model("Cat", {"id": Number, "name": {"type": String, "index": {"global": true, "rangeKey": "age"}}, "age": Number});
+					const query = Model.query("name").eq("Charlie").where("age").eq(10);
 					await callType.func(query.exec).bind(query)();
-					expect(queryParams).to.eql({"KeyConditions": {
-						"id": {
-							"ComparisonOperator": "BETWEEN",
-							"AttributeValueList": [
-								{"N": "1"},
-								{"N": "3"}
-							]
-						}
-					}, "TableName": "Cat", "QueryFilter": {}});
+					expect(queryParams).to.eql({
+						"TableName": "Cat",
+						"IndexName": "nameGlobalIndex",
+						"ExpressionAttributeNames": {
+							"#qha": "name",
+							"#qra": "age"
+						},
+						"ExpressionAttributeValues": {
+							":qhv": {"S": "Charlie"},
+							":qrv": {"N": "10"}
+						},
+						"KeyConditionExpression": "#qha = :qhv AND #qra = :qrv"
+					});
 				});
 
 				it("Should throw error from AWS", () => {
@@ -247,8 +291,8 @@ describe("Query", () => {
 
 		it("Should set correct property", () => {
 			expect(Model.query().settings.pending).to.eql({});
-			expect(Model.query().where("id").settings.pending).to.eql({"key": "id", "queryCondition": true});
-			expect(Model.query().where("id").where("name").settings.pending).to.eql({"key": "name", "queryCondition": true});
+			expect(Model.query().where("id").settings.pending).to.eql({"key": "id", "queryCondition": "range"});
+			expect(Model.query().where("id").where("name").settings.pending).to.eql({"key": "name", "queryCondition": "range"});
 		});
 	});
 
@@ -268,28 +312,6 @@ describe("Query", () => {
 		});
 	});
 
-	describe("query.null", () => {
-		it("Should be a function", () => {
-			expect(Model.query().null).to.be.a("function");
-		});
-
-		it("Should return an instance of query", () => {
-			expect(Model.query().null()).to.be.a.instanceof(Model.query.carrier);
-		});
-
-		it("Should set correct settings on the query object", () => {
-			const query = Model.query().filter("id").null();
-			expect(query.settings.filters).to.eql({"id": {"type": "NULL", "value": []}});
-			expect(query.settings.pending).to.eql({});
-		});
-
-		it("Should set correct settings on the query object with not()", () => {
-			const query = Model.query().filter("id").not().null();
-			expect(query.settings.filters).to.eql({"id": {"type": "NOT_NULL", "value": []}});
-			expect(query.settings.pending).to.eql({});
-		});
-	});
-
 	describe("query.eq", () => {
 		it("Should be a function", () => {
 			expect(Model.query().eq).to.be.a("function");
@@ -311,11 +333,42 @@ describe("Query", () => {
 			expect(query.settings.pending).to.eql({});
 		});
 
-		it("Should have same options as query.null for empty string, null, or undefined as value", () => {
-			expect(Model.query().filter("id").eq().settings.filters).to.eql(Model.query().filter("id").null().settings.filters);
-			expect(Model.query().filter("id").eq("").settings.filters).to.eql(Model.query().filter("id").null().settings.filters);
-			expect(Model.query().filter("id").eq(null).settings.filters).to.eql(Model.query().filter("id").null().settings.filters);
-			expect(Model.query().filter("id").eq(undefined).settings.filters).to.eql(Model.query().filter("id").null().settings.filters);
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").eq(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 = :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
+
+		it("Should send correct request with not()", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").not().eq(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 <> :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
 		});
 	});
 
@@ -339,6 +392,25 @@ describe("Query", () => {
 			expect(query.settings.filters).to.eql({"id": {"type": "GE", "value": "test"}});
 			expect(query.settings.pending).to.eql({});
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").lt(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 < :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.le", () => {
@@ -361,6 +433,25 @@ describe("Query", () => {
 			expect(query.settings.filters).to.eql({"id": {"type": "GT", "value": "test"}});
 			expect(query.settings.pending).to.eql({});
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").le(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 <= :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.gt", () => {
@@ -383,6 +474,25 @@ describe("Query", () => {
 			expect(query.settings.filters).to.eql({"id": {"type": "LE", "value": "test"}});
 			expect(query.settings.pending).to.eql({});
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").gt(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 > :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.ge", () => {
@@ -405,6 +515,25 @@ describe("Query", () => {
 			expect(query.settings.filters).to.eql({"id": {"type": "LT", "value": "test"}});
 			expect(query.settings.pending).to.eql({});
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").ge(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "#a1 >= :v1",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.beginsWith", () => {
@@ -426,6 +555,25 @@ describe("Query", () => {
 			const query = () => Model.query().filter("id").not().beginsWith("test");
 			expect(query).to.throw("BEGINS_WITH can not follow not()");
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").beginsWith("test").exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"S": "test"}
+				},
+				"FilterExpression": "begins_with (#a1, :v1)",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.contains", () => {
@@ -448,6 +596,44 @@ describe("Query", () => {
 			expect(query.settings.filters).to.eql({"id": {"type": "NOT_CONTAINS", "value": "test"}});
 			expect(query.settings.pending).to.eql({});
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").contains(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "contains (#a1, :v1)",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
+
+		it("Should send correct request with not()", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").not().contains(5).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1": {"N": "5"}
+				},
+				"FilterExpression": "NOT contains (#a1, :v1)",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.in", () => {
@@ -460,15 +646,57 @@ describe("Query", () => {
 		});
 
 		it("Should set correct settings on the query object", () => {
-			const query = Model.query().filter("id").in("test");
-			expect(query.settings.filters).to.eql({"id": {"type": "IN", "value": "test"}});
+			const query = Model.query().filter("id").in(["test", "other"]);
+			expect(query.settings.filters).to.eql({"id": {"type": "IN", "value": ["test", "other"]}});
 			expect(query.settings.pending).to.eql({});
 		});
 
 		it("Should throw error with not()", () => {
-			const query = () => Model.query().filter("id").not().in("test");
+			const query = () => Model.query().filter("id").not().in(["test", "other"]);
 			expect(query).to.throw("IN can not follow not()");
 		});
+
+		it("Should send correct request", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").in([10, 20]).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1-1": {"N": "10"},
+					":v1-2": {"N": "20"}
+				},
+				"FilterExpression": "#a1 IN (:v1-1, :v1-2)",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
+
+		it("Should send correct request for many values", async () => {
+			queryPromiseResolver = () => ({"Items": []});
+			await Model.query("name").eq("Charlie").filter("age").in([10, 20, 30, 40]).exec();
+			expect(queryParams).to.eql({
+				"TableName": "Cat",
+				"IndexName": "nameGlobalIndex",
+				"ExpressionAttributeNames": {
+					"#qha": "name",
+					"#a1": "age"
+				},
+				"ExpressionAttributeValues": {
+					":qhv": {"S": "Charlie"},
+					":v1-1": {"N": "10"},
+					":v1-2": {"N": "20"},
+					":v1-3": {"N": "30"},
+					":v1-4": {"N": "40"}
+				},
+				"FilterExpression": "#a1 IN (:v1-1, :v1-2, :v1-3, :v1-4)",
+				"KeyConditionExpression": "#qha = :qhv"
+			});
+		});
 	});
 
 	describe("query.between", () => {
@@ -504,7 +732,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().limit(5).exec();
+			await Model.query("name").eq("Charlie").limit(5).exec();
 			expect(queryParams.Limit).to.eql(5);
 		});
 	});
@@ -521,7 +749,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().startAt({"id": 5}).exec();
+			await Model.query("name").eq("Charlie").startAt({"id": 5}).exec();
 			expect(queryParams.ExclusiveStartKey).to.eql({"id": {"N": "5"}});
 		});
 
@@ -532,7 +760,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec if passing in DynamoDB object", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().startAt({"id": {"N": "5"}}).exec();
+			await Model.query("name").eq("Charlie").startAt({"id": {"N": "5"}}).exec();
 			expect(queryParams.ExclusiveStartKey).to.eql({"id": {"N": "5"}});
 		});
 	});
@@ -549,7 +777,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().attributes(["id"]).exec();
+			await Model.query("name").eq("Charlie").attributes(["id"]).exec();
 			expect(queryParams.AttributesToGet).to.eql(["id"]);
 		});
 	});
@@ -576,13 +804,13 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().count().exec();
+			await Model.query("name").eq("Charlie").count().exec();
 			expect(queryParams.Select).to.eql("COUNT");
 		});
 
 		it("Should return correct result on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}], "Count": 1, "QueriedCount": 1, "LastEvaluatedKey": {"id": {"N": "5"}}});
-			const result = await Model.query().count().exec();
+			const result = await Model.query("name").eq("Charlie").count().exec();
 			expect(result).to.eql({"count": 1, "queriedCount": 1});
 		});
 	});
@@ -599,7 +827,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().consistent().exec();
+			await Model.query("name").eq("Charlie").consistent().exec();
 			expect(queryParams.ConsistentRead).to.be.true;
 		});
 	});
@@ -616,7 +844,7 @@ describe("Query", () => {
 
 		it("Should send correct request on query.exec", async () => {
 			queryPromiseResolver = () => ({"Items": []});
-			await Model.query().using("customIndex").exec();
+			await Model.query("name").eq("Charlie").using("customIndex").exec();
 			expect(queryParams.IndexName).to.eql("customIndex");
 		});
 	});
@@ -646,7 +874,7 @@ describe("Query", () => {
 			queryPromiseResolver = async () => ({"Items": [], "LastEvaluatedKey": {"id": {"S": "test"}}});
 
 			const start = Date.now();
-			await Model.query().all(50, 2).exec();
+			await Model.query("name").eq("Charlie").all(50, 2).exec();
 			const end = Date.now();
 			expect(end - start).to.be.above(99);
 		});
@@ -661,7 +889,7 @@ describe("Query", () => {
 				return obj;
 			};
 
-			const result = await Model.query().all().exec();
+			const result = await Model.query("name").eq("Charlie").all().exec();
 			expect(result.map((item) => ({...item}))).to.eql([{"id": 1}, {"id": 2}]);
 			expect(result.count).to.eql(2);
 			expect(result.queriedCount).to.eql(4);
diff --git a/test/Scan.js b/test/Scan.js
index 05663713..19db1b5a 100644
--- a/test/Scan.js
+++ b/test/Scan.js
@@ -130,36 +130,40 @@ describe("Scan", () => {
 				it("Should send correct request on scan.exec", async () => {
 					scanPromiseResolver = () => ({"Items": []});
 					await callType.func(Model.scan().exec).bind(Model.scan())();
-					expect(scanParams).to.eql({"ScanFilter": {}, "TableName": "Cat"});
+					expect(scanParams).to.eql({"TableName": "Cat"});
 				});
 
 				it("Should send correct request on scan.exec with filters", async () => {
 					scanPromiseResolver = () => ({"Items": []});
 					const scan = Model.scan().filter("id").eq("test");
 					await callType.func(scan.exec).bind(scan)();
-					expect(scanParams).to.eql({"ScanFilter": {
-						"id": {
-							"ComparisonOperator": "EQ",
-							"AttributeValueList": [
-								{"S": "test"}
-							]
-						}
-					}, "TableName": "Cat"});
+					expect(scanParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "id"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {"S": "test"}
+						},
+						"FilterExpression": "#a0 = :v0",
+						"TableName": "Cat"
+					});
 				});
 
 				it("Should send correct request on scan.exec with filters and multiple values", async () => {
 					scanPromiseResolver = () => ({"Items": []});
 					const scan = Model.scan().filter("id").between(1, 3);
 					await callType.func(scan.exec).bind(scan)();
-					expect(scanParams).to.eql({"ScanFilter": {
-						"id": {
-							"ComparisonOperator": "BETWEEN",
-							"AttributeValueList": [
-								{"N": "1"},
-								{"N": "3"}
-							]
-						}
-					}, "TableName": "Cat"});
+					expect(scanParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "id"
+						},
+						"ExpressionAttributeValues": {
+							":v0-1": {"N": "1"},
+							":v0-2": {"N": "3"}
+						},
+						"FilterExpression": "#a0 BETWEEN :v0-1 AND :v0-2",
+						"TableName": "Cat"
+					});
 				});
 
 				it("Should throw error from AWS", () => {
@@ -233,28 +237,6 @@ describe("Scan", () => {
 		});
 	});
 
-	describe("scan.null", () => {
-		it("Should be a function", () => {
-			expect(Model.scan().null).to.be.a("function");
-		});
-
-		it("Should return an instance of scan", () => {
-			expect(Model.scan().null()).to.be.a.instanceof(Model.scan.carrier);
-		});
-
-		it("Should set correct settings on the scan object", () => {
-			const scan = Model.scan().filter("id").null();
-			expect(scan.settings.filters).to.eql({"id": {"type": "NULL", "value": []}});
-			expect(scan.settings.pending).to.eql({});
-		});
-
-		it("Should set correct settings on the scan object with not()", () => {
-			const scan = Model.scan().filter("id").not().null();
-			expect(scan.settings.filters).to.eql({"id": {"type": "NOT_NULL", "value": []}});
-			expect(scan.settings.pending).to.eql({});
-		});
-	});
-
 	describe("scan.eq", () => {
 		it("Should be a function", () => {
 			expect(Model.scan().eq).to.be.a("function");
@@ -275,13 +257,6 @@ describe("Scan", () => {
 			expect(scan.settings.filters).to.eql({"id": {"type": "NE", "value": "test"}});
 			expect(scan.settings.pending).to.eql({});
 		});
-
-		it("Should have same options as scan.null for empty string, null, or undefined as value", () => {
-			expect(Model.scan().filter("id").eq().settings.filters).to.eql(Model.scan().filter("id").null().settings.filters);
-			expect(Model.scan().filter("id").eq("").settings.filters).to.eql(Model.scan().filter("id").null().settings.filters);
-			expect(Model.scan().filter("id").eq(null).settings.filters).to.eql(Model.scan().filter("id").null().settings.filters);
-			expect(Model.scan().filter("id").eq(undefined).settings.filters).to.eql(Model.scan().filter("id").null().settings.filters);
-		});
 	});
 
 	describe("scan.lt", () => {
diff --git a/test/Schema.js b/test/Schema.js
index 4047c7c8..75714f8c 100644
--- a/test/Schema.js
+++ b/test/Schema.js
@@ -300,13 +300,9 @@ describe("Schema", () => {
 								"ProjectionType": "ALL"
 							},
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							]
 						}
@@ -344,13 +340,9 @@ describe("Schema", () => {
 								"ProjectionType": "ALL"
 							},
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							]
 						},
@@ -360,13 +352,9 @@ describe("Schema", () => {
 								"ProjectionType": "ALL"
 							},
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "name",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							]
 						}
@@ -400,13 +388,9 @@ describe("Schema", () => {
 								"ProjectionType": "ALL"
 							},
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							]
 						}
@@ -447,8 +431,56 @@ describe("Schema", () => {
 								}
 							],
 							"ProvisionedThroughput": {
-								"ReadCapacityUnits": "5",
-								"WriteCapacityUnits": "5"
+								"ReadCapacityUnits": 5,
+								"WriteCapacityUnits": 5
+							},
+							"Projection": {
+								"ProjectionType": "ALL"
+							}
+						}
+					]
+				}
+			},
+			{
+				"name": "Should return correct result with global index as object with rangeKey as different property",
+				"input": {"id": String, "age": {"type": Number, "index": {"name": "ageIndex", "global": true, "rangeKey": "type", "project": true, "throughput": 5}}, "type": String},
+				"output": {
+					"AttributeDefinitions": [
+						{
+							"AttributeName": "id",
+							"AttributeType": "S"
+						},
+						{
+							"AttributeName": "age",
+							"AttributeType": "N"
+						},
+						{
+							"AttributeName": "type",
+							"AttributeType": "S"
+						}
+					],
+					"KeySchema": [
+						{
+							"AttributeName": "id",
+							"KeyType": "HASH"
+						}
+					],
+					"GlobalSecondaryIndexes": [
+						{
+							"IndexName": "ageIndex",
+							"KeySchema": [
+								{
+									"AttributeName": "age",
+									"KeyType": "HASH"
+								},
+								{
+									"AttributeName": "type",
+									"KeyType": "RANGE"
+								}
+							],
+							"ProvisionedThroughput": {
+								"ReadCapacityUnits": 5,
+								"WriteCapacityUnits": 5
 							},
 							"Projection": {
 								"ProjectionType": "ALL"
@@ -482,12 +514,44 @@ describe("Schema", () => {
 							"IndexName": "ageIndex",
 							"KeySchema": [
 								{
-									"AttributeName": "id",
+									"AttributeName": "age",
 									"KeyType": "HASH"
-								},
+								}
+							],
+							"Projection": {
+								"ProjectionType": "ALL"
+							}
+						}
+					]
+				}
+			},
+			{
+				"name": "Should return correct result with index and project as true",
+				"input": {"id": String, "age": {"type": Number, "index": {"name": "ageIndex", "global": true, "project": null}}},
+				"output": {
+					"AttributeDefinitions": [
+						{
+							"AttributeName": "id",
+							"AttributeType": "S"
+						},
+						{
+							"AttributeName": "age",
+							"AttributeType": "N"
+						}
+					],
+					"KeySchema": [
+						{
+							"AttributeName": "id",
+							"KeyType": "HASH"
+						}
+					],
+					"GlobalSecondaryIndexes": [
+						{
+							"IndexName": "ageIndex",
+							"KeySchema": [
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							],
 							"Projection": {
@@ -521,13 +585,9 @@ describe("Schema", () => {
 						{
 							"IndexName": "ageIndex",
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							],
 							"Projection": {
@@ -561,13 +621,9 @@ describe("Schema", () => {
 						{
 							"IndexName": "ageIndex",
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							],
 							"Projection": {
@@ -602,13 +658,9 @@ describe("Schema", () => {
 						{
 							"IndexName": "ageLocalIndex",
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							],
 							"Projection": {
@@ -642,13 +694,9 @@ describe("Schema", () => {
 						{
 							"IndexName": "ageGlobalIndex",
 							"KeySchema": [
-								{
-									"AttributeName": "id",
-									"KeyType": "HASH"
-								},
 								{
 									"AttributeName": "age",
-									"KeyType": "RANGE"
+									"KeyType": "HASH"
 								}
 							],
 							"Projection": {
@@ -662,7 +710,7 @@ describe("Schema", () => {
 
 		tests.forEach((test) => {
 			it(test.name, async () => {
-				expect(await (new Schema(test.input).getCreateTableAttributeParams())).to.eql(test.output);
+				expect(await (new Schema(test.input).getCreateTableAttributeParams({"options": {"throughput": "ON_DEMAND"}}))).to.eql(test.output);
 			});
 		});
 	});