[ledger][sledge] Add inequalities to Queries.
Queries now store a mapping from fieldPaths to QueryFieldComparison.
A helper to facilitate building queries will be introduced in follow up
CLs.
TEST=dart_sledge_tests
Change-Id: I958ed7a079889d83a5203f6ea681094cc5bad51a
diff --git a/public/dart/sledge/lib/sledge.dart b/public/dart/sledge/lib/sledge.dart
index 4d482b3..5a8b297 100644
--- a/public/dart/sledge/lib/sledge.dart
+++ b/public/dart/sledge/lib/sledge.dart
@@ -7,6 +7,7 @@
export 'src/document/document_id.dart';
export 'src/query/index_definition.dart';
export 'src/query/query.dart';
+export 'src/query/query_field_comparison.dart';
export 'src/schema/base_type.dart';
export 'src/schema/schema.dart';
export 'src/sledge.dart';
diff --git a/public/dart/sledge/lib/src/conflict_resolver/conflict_resolver.dart b/public/dart/sledge/lib/src/conflict_resolver/conflict_resolver.dart
index 5c7483d..a22e0d7 100644
--- a/public/dart/sledge/lib/src/conflict_resolver/conflict_resolver.dart
+++ b/public/dart/sledge/lib/src/conflict_resolver/conflict_resolver.dart
@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// import 'dart:async';
import 'dart:typed_data';
import 'package:fidl/fidl.dart';
diff --git a/public/dart/sledge/lib/src/query/field_value.dart b/public/dart/sledge/lib/src/query/field_value.dart
index 988c99d..edf14fd 100644
--- a/public/dart/sledge/lib/src/query/field_value.dart
+++ b/public/dart/sledge/lib/src/query/field_value.dart
@@ -13,17 +13,12 @@
import '../schema/types/trivial_types.dart';
/// Stores the value of a field in queries.
-abstract class FieldValue {
+abstract class FieldValue implements Comparable<Value> {
/// The hash of the field's value.
Uint8List get hash;
/// Returns whether this can be compared to [type].
bool comparableTo(BaseType type);
-
- /// Returns whether this is equal to [value].
- /// If `value` is not instantiated from a BaseType comparable to this,
- /// an exception is thrown.
- bool equalsTo(Value value);
}
/// Template to ease the implementation of FieldValue specializations.
@@ -51,14 +46,12 @@
}
@override
- bool equalsTo(Value documentValue) {
+ int compareTo(Value documentValue) {
if (documentValue is PosNegCounterValue<int>) {
- PosNegCounterValue<int> downcastedValue = documentValue;
- return _value == downcastedValue.value;
+ return _value.compareTo(documentValue.value);
}
if (documentValue is LastOneWinsValue<int>) {
- LastOneWinsValue<int> downcastedValue = documentValue;
- return _value == downcastedValue.value;
+ return _value.compareTo(documentValue.value);
}
throw new ArgumentError('`documentValue` is not comparable to a integer.');
}
diff --git a/public/dart/sledge/lib/src/query/query.dart b/public/dart/sledge/lib/src/query/query.dart
index 0e97653..62bd39b 100644
--- a/public/dart/sledge/lib/src/query/query.dart
+++ b/public/dart/sledge/lib/src/query/query.dart
@@ -9,35 +9,37 @@
import '../schema/schema.dart';
import '../storage/kv_encoding.dart' as sledge_storage;
import '../uint8list_ops.dart' as utils;
-import 'field_value.dart';
+import 'query_field_comparison.dart';
/// Represents a query for retrieving documents from Sledge.
-/// TODO: Add support for inequality.
class Query {
final Schema _schema;
- /// Stores the value each document's field needs to have in order to be
- /// returned by the query.
- SplayTreeMap<String, FieldValue> _equalities;
+ /// Stores a QueryFieldComparison each document's field needs respect in
+ /// order to be returned by the query.
+ SplayTreeMap<String, QueryFieldComparison> _comparisons;
/// Default constructor.
- /// `schema` describes the type of documents the query returns.
- /// `equalities` associates field names with their expected values.
- /// Throws an exception if `equalities` references a field not part of
- /// `schema`.
- Query(this._schema, {Map<String, FieldValue> equalities}) {
- // TODO: throw an exception if `equalities` references fields not part of
- // `schema`.
- equalities ??= <String, FieldValue>{};
- equalities.forEach((fieldPath, fieldValue) {
- final expectedType = schema.fieldAtPath(fieldPath);
- if (!fieldValue.comparableTo(expectedType)) {
- String runtimeType = expectedType.runtimeType.toString();
- throw new ArgumentError(
- 'Field `$fieldPath` of type `$runtimeType` is not comparable with `$fieldValue`.');
+ /// [schema] describes the type of documents the query returns.
+ /// [comparisons] associates field names with constraints documents returned
+ /// by the query respects.
+ /// Throws an exception if [comparisons] references a field not part of
+ /// [schema], or if multiple inequalities are present.
+ Query(this._schema, {Map<String, QueryFieldComparison> comparisons}) {
+ comparisons ??= <String, QueryFieldComparison>{};
+ final fieldsWithInequalities = <String>[];
+ comparisons.forEach((fieldPath, comparison) {
+ if (comparison.comparisonType != ComparisonType.equal) {
+ fieldsWithInequalities.add(fieldPath);
}
+ _checkComparisonWithField(fieldPath, comparison);
});
- _equalities = new SplayTreeMap<String, FieldValue>.from(equalities);
+ if (fieldsWithInequalities.length > 1) {
+ throw new ArgumentError(
+ 'Queries can have at most one inequality. Inequalities founds: $fieldsWithInequalities.');
+ }
+ _comparisons =
+ new SplayTreeMap<String, QueryFieldComparison>.from(comparisons);
}
/// The Schema of documents returned by this query.
@@ -46,7 +48,7 @@
/// Returns whether this query filters the Documents based on the content of
/// their fields.
bool filtersDocuments() {
- return _equalities.isNotEmpty;
+ return _comparisons.isNotEmpty;
}
/// The prefix of the key values encoding the index that helps compute the
@@ -54,16 +56,20 @@
/// Must only be called if `filtersDocuments()` returns true.
Uint8List prefixInIndex() {
assert(filtersDocuments());
- List<Uint8List> hashes = <Uint8List>[];
- _equalities.forEach((field, value) {
- hashes.add(value.hash);
+ final equalityValueHashes = <Uint8List>[];
+ _comparisons.forEach((field, comparison) {
+ if (comparison.comparisonType == ComparisonType.equal) {
+ equalityValueHashes.add(utils.getUint8ListFromString(field));
+ }
});
- Uint8List equalityHash = utils.hash(utils.concatListOfUint8Lists(hashes));
+ Uint8List equalityHash =
+ utils.hash(utils.concatListOfUint8Lists(equalityValueHashes));
// TODO: get the correct index hash.
Uint8List indexHash = new Uint8List(20);
+ // TODO: take into account the inequality to compute the prefix.
Uint8List prefix = utils.concatListOfUint8Lists([
sledge_storage.prefixForType(sledge_storage.KeyValueType.indexEntry),
indexHash,
@@ -81,11 +87,21 @@
throw new ArgumentError(
'The Document `doc` is of a incorrect Schema type.');
}
- for (final fieldName in _equalities.keys) {
- if (!_equalities[fieldName].equalsTo(doc[fieldName])) {
+ for (final fieldName in _comparisons.keys) {
+ if (!_comparisons[fieldName].valueMatchesComparison(doc[fieldName])) {
return false;
}
}
return true;
}
+
+ void _checkComparisonWithField(
+ String fieldPath, QueryFieldComparison comparison) {
+ final expectedType = _schema.fieldAtPath(fieldPath);
+ if (!comparison.comparisonValue.comparableTo(expectedType)) {
+ String runtimeType = expectedType.runtimeType.toString();
+ throw new ArgumentError(
+ 'Field `$fieldPath` of type `$runtimeType` is not comparable with `$comparison.comparisonValue`.');
+ }
+ }
}
diff --git a/public/dart/sledge/lib/src/query/query_field_comparison.dart b/public/dart/sledge/lib/src/query/query_field_comparison.dart
new file mode 100644
index 0000000..df7d543
--- /dev/null
+++ b/public/dart/sledge/lib/src/query/query_field_comparison.dart
@@ -0,0 +1,60 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../document/value.dart';
+import 'field_value.dart';
+
+/// The types of comparison possible in queries on the field of a document.
+enum ComparisonType {
+ /// Field must be less than a given value.
+ less,
+
+ /// Field must be less than or equal to a given value.
+ lessOrEqual,
+
+ /// Field must be equal to a given value.
+ equal,
+
+ /// Field must be greater than or equal to a given value.
+ greaterOrEqual,
+
+ /// Field must be greater than a given value.
+ greater
+}
+
+/// Holds the information necessary to compare to a Value:
+/// It holds a comparison type (<, <=, ==, >, >=), and a value (e.g. 42).
+class QueryFieldComparison {
+ FieldValue _comparisonValue;
+ ComparisonType _comparisonType;
+
+ /// Default constructor.
+ QueryFieldComparison(this._comparisonValue, this._comparisonType);
+
+ /// Returns how [value] compares to the value stored in [this].
+ bool valueMatchesComparison(Value value) {
+ int comparisonResult = _comparisonValue.compareTo(value);
+
+ // the value in [_comparisonValue] is equal to [value].
+ if (comparisonResult == 0) {
+ return _comparisonType == ComparisonType.equal ||
+ _comparisonType == ComparisonType.lessOrEqual ||
+ _comparisonType == ComparisonType.greaterOrEqual;
+ }
+ // the value in [_comparisonValue] is greater than [value].
+ if (comparisonResult > 0) {
+ return _comparisonType == ComparisonType.lessOrEqual ||
+ _comparisonType == ComparisonType.less;
+ }
+ // the value in [_comparisonValue] is less than [value].
+ return _comparisonType == ComparisonType.greaterOrEqual ||
+ _comparisonType == ComparisonType.greater;
+ }
+
+ /// Returns the value stored in [this].
+ FieldValue get comparisonValue => _comparisonValue;
+
+ /// Returns the type of comparison.
+ ComparisonType get comparisonType => _comparisonType;
+}
diff --git a/public/dart/sledge/lib/src/uint8list_ops.dart b/public/dart/sledge/lib/src/uint8list_ops.dart
index 87fbf99..b36f5e0 100644
--- a/public/dart/sledge/lib/src/uint8list_ops.dart
+++ b/public/dart/sledge/lib/src/uint8list_ops.dart
@@ -10,6 +10,8 @@
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
+import 'document/values/converter.dart';
+
// TODO: consider short function names and importing with prefix.
/// Concatenate two byte arrays.
@@ -47,6 +49,12 @@
return new Uint8List.fromList(utf8.encode(string));
}
+/// Returns a Uint8List created from the binary encoding of [number].
+Uint8List getUint8ListFromNumber(num number) {
+ IntConverter converter = new IntConverter();
+ return converter.serialize(number);
+}
+
/// Returns a new HashMap with Uint8Lists as a keys.
/// Note: The type T is enforced only at compile time.
HashMap<Uint8List, T> newUint8ListMap<T>() {
diff --git a/public/dart/sledge/test/get_documents_test.dart b/public/dart/sledge/test/get_documents_test.dart
index e8c75ef..3a37a8d 100644
--- a/public/dart/sledge/test/get_documents_test.dart
+++ b/public/dart/sledge/test/get_documents_test.dart
@@ -78,8 +78,11 @@
Schema schema = _newSchema3();
Sledge sledge = newSledgeForTesting();
await sledge.runInTransaction(() async {
- final equalities = <String, FieldValue>{'i1': new IntFieldValue(42)};
- final query = new Query(schema, equalities: equalities);
+ final comparisons = <String, QueryFieldComparison>{
+ 'i1': new QueryFieldComparison(
+ new IntFieldValue(42), ComparisonType.equal)
+ };
+ final query = new Query(schema, comparisons: comparisons);
final docs = await sledge.getDocuments(query);
expect(docs.length, equals(0));
});
@@ -111,33 +114,84 @@
doc5['i1'].value = 2;
doc5['i2'].value = 20;
});
- // Run 3 queries and verify that the results are correct.
+ // Verify the resuts of queries with equalities.
await sledge.runInTransaction(() async {
{
- final equalities = <String, FieldValue>{'i1': new IntFieldValue(1)};
- final query = new Query(schema, equalities: equalities);
+ final comparisons = <String, QueryFieldComparison>{
+ 'i1': new QueryFieldComparison(
+ new IntFieldValue(1), ComparisonType.equal)
+ };
+ final query = new Query(schema, comparisons: comparisons);
final docs = await sledge.getDocuments(query);
expect(docs.length, equals(2));
expect(docs, containsAll([doc1, doc3]));
}
{
- final equalities = <String, FieldValue>{'i2': new IntFieldValue(30)};
- final query = new Query(schema, equalities: equalities);
+ final comparisons = <String, QueryFieldComparison>{
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(30), ComparisonType.equal)
+ };
+ final query = new Query(schema, comparisons: comparisons);
final docs = await sledge.getDocuments(query);
expect(docs.length, equals(2));
expect(docs, containsAll([doc3, doc4]));
}
{
- final equalities = <String, FieldValue>{
- 'i1': new IntFieldValue(2),
- 'i2': new IntFieldValue(30)
+ final comparisons = <String, QueryFieldComparison>{
+ 'i1': new QueryFieldComparison(
+ new IntFieldValue(2), ComparisonType.equal),
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(30), ComparisonType.equal)
};
- final query = new Query(schema, equalities: equalities);
+ final query = new Query(schema, comparisons: comparisons);
final docs = await sledge.getDocuments(query);
expect(docs.length, equals(1));
expect(docs, containsAll([doc4]));
}
});
+ // Verify the resuts of queries with inequalities.
+ await sledge.runInTransaction(() async {
+ final less = <String, QueryFieldComparison>{
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(20), ComparisonType.less)
+ };
+ final lessOrEqual = <String, QueryFieldComparison>{
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(20), ComparisonType.lessOrEqual)
+ };
+ final greaterOrEqual = <String, QueryFieldComparison>{
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(20), ComparisonType.greaterOrEqual)
+ };
+ final greater = <String, QueryFieldComparison>{
+ 'i2': new QueryFieldComparison(
+ new IntFieldValue(20), ComparisonType.greater)
+ };
+ {
+ final docs =
+ await sledge.getDocuments(new Query(schema, comparisons: less));
+ expect(docs.length, equals(1));
+ expect(docs, containsAll([doc1]));
+ }
+ {
+ final docs = await sledge
+ .getDocuments(new Query(schema, comparisons: lessOrEqual));
+ expect(docs.length, equals(3));
+ expect(docs, containsAll([doc1, doc2, doc5]));
+ }
+ {
+ final docs = await sledge
+ .getDocuments(new Query(schema, comparisons: greaterOrEqual));
+ expect(docs.length, equals(4));
+ expect(docs, containsAll([doc2, doc3, doc4, doc5]));
+ }
+ {
+ final docs = await sledge
+ .getDocuments(new Query(schema, comparisons: greater));
+ expect(docs.length, equals(2));
+ expect(docs, containsAll([doc3, doc4]));
+ }
+ });
});
});
}
diff --git a/public/dart/sledge/test/query/query_test.dart b/public/dart/sledge/test/query/query_test.dart
index 048604e..a270231 100644
--- a/public/dart/sledge/test/query/query_test.dart
+++ b/public/dart/sledge/test/query/query_test.dart
@@ -13,6 +13,7 @@
final schemaDescription = <String, BaseType>{
'a': new Integer(),
'b': new LastOneWinsString(),
+ 'c': new Integer(),
};
return new Schema(schemaDescription);
}
@@ -29,47 +30,90 @@
test('Verify that creating invalid queries throws an error', () async {
Schema schema = _newSchema();
- final equalitiesWithNonExistantField = <String, FieldValue>{
- 'foo': new IntFieldValue(42)
+
+ // Test invalid comparisons
+ final comparisonWithNonExistantField = <String, QueryFieldComparison>{
+ 'foo':
+ new QueryFieldComparison(new IntFieldValue(42), ComparisonType.equal)
};
- expect(() => new Query(schema, equalities: equalitiesWithNonExistantField),
+ expect(() => new Query(schema, comparisons: comparisonWithNonExistantField),
throwsArgumentError);
- final equalitiesWithWrongType = <String, FieldValue>{
- 'b': new IntFieldValue(42)
+ final comparisonWithWrongType = <String, QueryFieldComparison>{
+ 'b': new QueryFieldComparison(new IntFieldValue(42), ComparisonType.equal)
};
- expect(() => new Query(schema, equalities: equalitiesWithWrongType),
+ expect(() => new Query(schema, comparisons: comparisonWithWrongType),
+ throwsArgumentError);
+
+ // Test too many inequalities
+ final comparisonWithMultipleInequalities = <String, QueryFieldComparison>{
+ 'a': new QueryFieldComparison(
+ new IntFieldValue(42), ComparisonType.greater),
+ 'c': new QueryFieldComparison(
+ new IntFieldValue(42), ComparisonType.greater)
+ };
+ expect(
+ () =>
+ new Query(schema, comparisons: comparisonWithMultipleInequalities),
throwsArgumentError);
});
test('Verify `filtersDocuments`', () async {
Schema schema = _newSchema();
- final equalities = <String, FieldValue>{'a': new IntFieldValue(42)};
- final query2 = new Query(schema);
- expect(query2.filtersDocuments(), equals(false));
- final query1 = new Query(schema, equalities: equalities);
- expect(query1.filtersDocuments(), equals(true));
+ final query1 = new Query(schema);
+ expect(query1.filtersDocuments(), equals(false));
+
+ // Test with equalities
+ final equality = <String, QueryFieldComparison>{
+ 'a': new QueryFieldComparison(new IntFieldValue(42), ComparisonType.equal)
+ };
+ final query2 = new Query(schema, comparisons: equality);
+ expect(query2.filtersDocuments(), equals(true));
+
+ // Test with inequality
+ final inequality = <String, QueryFieldComparison>{
+ 'a': new QueryFieldComparison(
+ new IntFieldValue(42), ComparisonType.greater)
+ };
+ final query3 = new Query(schema, comparisons: inequality);
+ expect(query3.filtersDocuments(), equals(true));
});
test('Verify `documentMatchesQuery`', () async {
Sledge sledge = newSledgeForTesting();
Schema schema = _newSchema();
- final equalities = <String, FieldValue>{'a': new IntFieldValue(42)};
- final query1 = new Query(schema);
- final query2 = new Query(schema, equalities: equalities);
+ final equality = <String, QueryFieldComparison>{
+ 'a': new QueryFieldComparison(new IntFieldValue(42), ComparisonType.equal)
+ };
+ final inequality = <String, QueryFieldComparison>{
+ 'a': new QueryFieldComparison(
+ new IntFieldValue(42), ComparisonType.greater)
+ };
+ final queryWithoutFilter = new Query(schema);
+ final queryWithEqualities = new Query(schema, comparisons: equality);
+ final queryWithInequality = new Query(schema, comparisons: inequality);
await sledge.runInTransaction(() async {
Document doc1 = await sledge.getDocument(new DocumentId(schema));
doc1['a'].value = 1;
Document doc2 = await sledge.getDocument(new DocumentId(schema));
doc2['a'].value = 42;
- Document doc3 = await sledge.getDocument(new DocumentId(_newSchema2()));
- doc3['a'].value = 42;
- expect(query1.documentMatchesQuery(doc1), equals(true));
- expect(query1.documentMatchesQuery(doc2), equals(true));
- expect(query2.documentMatchesQuery(doc1), equals(false));
- expect(query2.documentMatchesQuery(doc2), equals(true));
- expect(() => query1.documentMatchesQuery(doc3), throwsArgumentError);
- expect(() => query2.documentMatchesQuery(doc3), throwsArgumentError);
+ Document doc3 = await sledge.getDocument(new DocumentId(schema));
+ doc3['a'].value = 43;
+ Document doc4 = await sledge.getDocument(new DocumentId(_newSchema2()));
+ doc4['a'].value = 42;
+ expect(queryWithoutFilter.documentMatchesQuery(doc1), equals(true));
+ expect(queryWithoutFilter.documentMatchesQuery(doc2), equals(true));
+ expect(queryWithEqualities.documentMatchesQuery(doc1), equals(false));
+ expect(queryWithEqualities.documentMatchesQuery(doc2), equals(true));
+ expect(queryWithInequality.documentMatchesQuery(doc1), equals(false));
+ expect(queryWithInequality.documentMatchesQuery(doc2), equals(false));
+ expect(queryWithInequality.documentMatchesQuery(doc3), equals(true));
+ expect(() => queryWithoutFilter.documentMatchesQuery(doc4),
+ throwsArgumentError);
+ expect(() => queryWithEqualities.documentMatchesQuery(doc4),
+ throwsArgumentError);
+ expect(() => queryWithInequality.documentMatchesQuery(doc4),
+ throwsArgumentError);
});
});
}