Couchbase Mobile supports SQL++ query strings using the SQL++ Query API. SQL++ is Couchbase’s declarative query language that extends SQL for JSON. The structure and semantics of the query format are based on that of Couchbase Server’s SQL++ query language — see N1QL Reference Guide and N1QL Data Model.
In this step of the learning path you will learn the fundamentals of:
While the demo app has a lot of functionality, this step will walk you through:
NOTE: This step assumes you completed the previous step
Batch operations
that loaded the sample data into the application. This part of the learning path will not work if you don't complete the previous steps.
Learn Couchbase Lite with Dart and Flutter
repository from GitHub.git clone https://github.com/couchbase-examples/flutter_cbl_learning_path.git
The sample app was loaded with a collection of Document
with a "type" property of "audit" in the previous sections of the learning path. Each document represents a item that that a team would would record with how many of those items they found in the warehouse while working on a project.
An example of a document would be:
{
"team": "team1",
"modifiedOn": "2022/10/12",
"documentType": "audit",
"createdBy": "demo@example.com",
"modifiedBy": "demo@example.com",
"projectId": "c4a8fbac-083a-4ad5-87e5-2fe1c03a3689",
"createdOn": "2022/10/12",
"auditId": "d3fb4d2e-0c75-4a85-b9b9-418a3b5f8303",
"notes": "Found item Langres Psion Ale - Sweet Ale with Langres flavors in warehouse",
"auditCount": 42330,
"stockItem": {
"name": "Langres Psion Ale",
"description": "Sweet Ale with Langres flavors",
"itemId": "e53dddf3-2058-4963-9293-66a87c84b29b",
"price": 52.76,
"style": "Imperial Stout"
}
}
When a "audit" item is retreived from the database it is stored within an data class of type Audit.
(explicitToJson: true)
class Audit {
String auditId;
String projectId;
StockItem? stockItem;
int auditCount;
String notes;
String documentType = "audit";
//security tracking
String team;
String createdBy;
String modifiedBy;
DateTime? createdOn;
DateTime? modifiedOn;
Audit({
required this.auditId,
required this.projectId,
required this.stockItem,
required this.auditCount,
required this.notes,
required this.team,
required this.createdBy,
required this.modifiedBy,
required this.createdOn,
required this.modifiedOn});
factory Audit.fromJson(Map<String, dynamic> json) => _$AuditFromJson(json);
Map<String, dynamic> toJson() => _$AuditToJson(this);
}
The SQL++ Query String API in Couchbase Lite is extensive. On the Developer - Database Information screen we display the number of audit documents found in the inventory database. The SQL++ Query String
API along with the count function was used to calculate this number. To see the Developer Information screen:
The "DeveloperInfoWidget" screen displays the count of how many documents are in the inventory database with the type set to 'audit'.
The DeveloperInfoWidget class obtains this information from the DevInfoBloc class, which emits this information. The DevInfoBloc
calls the AutitRepository which runs the query to calculate the count.
final auditCounter = await _auditRepository.count();
count
function.Future<int> count() async {
SQL++ Query String
API that will look for Documents that match the specified criteria.var count = 0;
try {
var attributeCount = 'count';
var db = _databaseProvider.inventoryDatabase;
if (db != null) {
var query = await AsyncQuery.fromN1ql(db,
'SELECT COUNT(*) AS count FROM _ AS item WHERE documentType="$auditDocumentType"'); // <1>
var result = await query.execute(); // <2>
var results = await result.allResults(); // <3>
count = results.first.integer(attributeCount); // <4>
}
} catch (e) {
debugPrint(e.toString());
}
return count;
execute()
method.allResults()
method to get all the results from the result.SELECT
clauseThere are several minor but notable behavior differences between SQL++ for Mobile queries and N1QL for Server. More information on the differences can be found in the SQL++ Server Differences documentation. In our example, we will be using the API to make a simple pattern matching query using the equalTo operator. This example will use Live Query with Dart to return a stream of changes.
From the "Login" screen, when a user logs in with the proper credentials they are presented with a list of projects there team is assigned to work on. From the project listing when a user taps on a project they are presented with a listing of audit items that was recorded as inventory items at the location of the project.
Open the audit_list_widget.dart file and locate the build
function.
The live query is an AsyncListenStreamAuditListBloc
.
TIP: Anytime documents change or are added to the database that satisify this query, the listen(change) would fire in the AuditListBloc and the AuditListBloc would fire the AuditListLoadedEvent that would update state. The
live query
API can dynmamically update your UI without the need for end user interaction, which will come in handy in future sections of the learning path when we review replication.
case DataStatus.loaded:
case DataStatus.changed:
return SafeArea(
child: ListView.builder(
itemCount: state.items.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () => {
routerService.routeTo(
ScreenRoute(
routeToScreen: RouteToScreen.auditEditor,
audit: state.items[index],
projectId: state.items[index].projectId)
)
},
child: AuditCard(item: state.items[index], routerService: routerService)
);
}));
List<Audit>
items is created, open the audit_list_bloc.dart file and locate the _onInitialize
function. Future<void> _onInitialize (
AuditListInitializeEvent event,
Emitter<AuditListState> emit) async {
AuditRepository.getDocuments
method.try {
emit(state.copyWith(status: DataStatus.loading));
// <1>
var stream = await _repository.getDocuments(event.projectId);
// <2>
if (stream != null) {
// <3>
stream.listen((change) async {
// <4>
var items = <Audit>[];
// <5>
var results = await change.results.allResults();
//<6>
for (var result in results) {
// <7>
var map = result.toPlainMap();
var dao = AuditDao.fromJson(map);
// <8>
items.add(dao.item);
}
// <9>
add(AuditListLoadedEvent(items: items));
});
}
} catch (e) {
emit(state.copyWith(status: DataStatus.error, error: e.toString()));
debugPrint(e.toString());
}
change
parameter.ResultSet
from the change object.isClosed
check prevents this from happening.Future<AsyncListenStream<QueryChange<ResultSet>>?>? getDocuments(String projectId) async {
// <1>
var query = await AsyncQuery.fromN1ql(db, "SELECT * FROM _ AS item WHERE documentType=\"audit\" AND projectId=\$projectId AND team=\$team");
//<2>
var parameters = Parameters();
parameters.setValue(projectId, name: "projectId");
parameters.setValue(team, name: "team");
//<3>
await query.setParameters(parameters);
return query.changes();
fromN1ql
methodCreating indexes for non-FTS based queries is optional. However, to speed up queries, you can create indexes on the properties that you would query against. Indexing is handled eagerly.
In the database_provider.dart file, locate the createAuditIndex
function.
We create an index on the type
, projectId
, and team
properties of the documents in the inventory database.
Future<void> _createAuditIndex() async {
final documentTypeExpression =
Expression.property(documentTypeAttributeName);
final projectIdExpression = Expression.property(projectIdAttributeName);
final teamExpression = Expression.property(teamAttributeName);
final valueIndexItems = {
ValueIndexItem.expression(documentTypeExpression),
ValueIndexItem.expression(projectIdExpression),
ValueIndexItem.expression(teamExpression),
};
final index = IndexBuilder.valueIndex(valueIndexItems);
var inventoryDb = inventoryDatabase;
if (inventoryDb != null) {
final indexes = await inventoryDb.indexes;
if (!(indexes.contains(auditIndexName))) {
await inventoryDb.createIndex(auditIndexName, index);
}
}
}
documentType
attribute.projectId
attribute.team
attribute.The SQL ++ Query String API supports serveral operators including the LIKE which can be used for string matching. We use the LIKE operator on the data editor screen for audits to find a stock item to add to a audit. Let's review the code.
NOTE: The like operator performs case sensitive matches. To perform case insensitive matching, use lowercase or uppercase functions to ensure all comparators have the same case, thereby removing the case issue.
On the Audit Editor screen we provide a link to the Stock Item Section screen. To see the Audit Editor screen:
Select Stock Item
Let's review the code for the Stock Item Section screen.
Open the audit_editor_form.dart file and locate the _StockItemSearchButton
class.
The build method adds an Elevated Button
The onPressed method adds the blocs StockItemSearchSubmitChangedEvent when a user taps on the Search button.
Widget build(BuildContext context) {
return BlocBuilder<StockItemSearchBloc, StockItemSearchState>(
builder: (context, state) {
if (state.status == FormEditorStatus.dataSaved ||
state.status == FormEditorStatus.cancelled) {
Navigator.of(context).pop();
return const Text('');
} else {
return Padding(
padding: const EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0),
child: ElevatedButton(
onPressed: () {
context
.read<StockItemSearchBloc>()
.add(const StockItemSearchSubmitChangedEvent());
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Search",
style: TextStyle(color: Colors.white, fontSize: 24.0),
))),
);
}
});
}
_onSubmitted
method.FutureOr<void> _onSubmitted(
StockItemSearchSubmitChangedEvent event,
Emitter<StockItemSearchState> emit) async {
if (state.searchName.isNotEmpty){
try {
var items = await _repository.search(state.searchName, state.searchDescription);
if (items.isNotEmpty){
emit(state.copyWith(error: '', status: FormEditorStatus.dataLoaded, items: items));
} else {
emit(state.copyWith(error: 'No stock items found matching criteria.', status: FormEditorStatus.error));
}
} catch (e){
emit(state.copyWith(error: e.toString(), status: FormEditorStatus.error));
}
} else {
emit(state.copyWith(error: 'Error - Search Name can\'t be blank', status: FormEditorStatus.error));
}
}
search
method.Future<List<StockItem>> search(
String? searchName,
String? searchDescription) async {
List<StockItem> items = [];
try {
if (searchName != null) {
var db = _databaseProvider.warehouseDatabase;
if (db != null) {
// <1>
var queryString = 'SELECT * FROM _ AS item WHERE documentType="$documentType" AND lower($nameAttributeName) LIKE (\'%\' || \$parameterName || \'%\')';
// <2>
var parameters = Parameters();
// <3>
parameters.setString(searchName.toLowerCase(), name: 'parameterName');
// <4>
if (searchDescription != null && searchDescription.isNotEmpty){
queryString = '$queryString AND lower($descriptionAttributeName) LIKE (\'%\' || \$parameterDescription || \'%\')';
parameters.setString(searchDescription, name: 'parameterDescription');
}
// <5>
var query = await AsyncQuery.fromN1ql(db, queryString);
// <6>
query.setParameters(parameters);
// <7>
var result = await query.execute();
var results = await result.allResults();
// <8>
for(var r in results){
var map = r.toPlainMap();
var stockItemDoa = StockItemDao.fromJson(map);
items.add(stockItemDoa.item);
}
}
}
} catch (e){
debugPrint(e.toString());
}
return items;
Congratulations on completing this step of our learning path!
This step of the learning path walked you through the SQL++ Query API in Couchbase Lite and used it to return documents from the database using serveral methods. Check out the following links for further documenation and continue on to the next step.