Skip to content

Commit f96f96f

Browse files
committed
chore(cloud_auth): Improve performance of ListUsers
Make use of Drift's [features](https://drift.simonbinder.eu/sql_api/drift_files/) to improve performance and correctly add emails + phone numbers to list queries.
1 parent 3b5d401 commit f96f96f

File tree

4 files changed

+117
-60
lines changed

4 files changed

+117
-60
lines changed

services/celest_cloud_auth/lib/src/database/schema/cloud_auth_users.drift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,38 @@ getUser:
117117
SELECT * FROM cloud_auth_users
118118
WHERE user_id = :user_id;
119119

120+
listUsers(
121+
:start_time AS DATETIME OR NULL,
122+
:offset AS INTEGER,
123+
:limit AS INTEGER
124+
):
125+
WITH rowed AS(
126+
SELECT
127+
ROW_NUMBER() OVER (ORDER BY create_time DESC) AS row_num,
128+
user_id
129+
FROM cloud_auth_users
130+
WHERE
131+
cloud_auth_users.create_time < coalesce(:start_time, unixepoch('now', '+1 second', 'subsec'))
132+
)
133+
SELECT
134+
row_num,
135+
cloud_auth_users.**,
136+
LIST(
137+
SELECT *
138+
FROM cloud_auth_user_emails
139+
WHERE user_id = rowed.user_id
140+
) AS emails,
141+
LIST(
142+
SELECT *
143+
FROM cloud_auth_user_phone_numbers
144+
WHERE user_id = rowed.user_id
145+
) AS phone_numbers
146+
FROM cloud_auth_users
147+
INNER JOIN rowed ON cloud_auth_users.user_id = rowed.user_id
148+
WHERE row_num > :offset
149+
ORDER BY $order_by
150+
LIMIT :limit;
151+
120152
deleteUser:
121153
DELETE FROM cloud_auth_users
122154
WHERE user_id = :user_id;

services/celest_cloud_auth/lib/src/database/schema/cloud_auth_users.drift.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,52 @@ class CloudAuthUsersDrift extends i4.ModularAccessor {
10721072
}).asyncMap(cloudAuthUsers.mapFromRow);
10731073
}
10741074

1075+
i0.Selectable<ListUsersResult> listUsers(
1076+
{DateTime? startTime,
1077+
required int offset,
1078+
ListUsers$orderBy? order_by,
1079+
required int limit}) {
1080+
var $arrayStartIndex = 4;
1081+
final generatedorder_by = $write(
1082+
order_by?.call(this.cloudAuthUsers) ?? const i0.OrderBy.nothing(),
1083+
startIndex: $arrayStartIndex);
1084+
$arrayStartIndex += generatedorder_by.amountOfVariables;
1085+
return customSelect(
1086+
'WITH rowed AS (SELECT ROW_NUMBER()OVER (ORDER BY create_time DESC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS row_num, user_id FROM cloud_auth_users WHERE cloud_auth_users.create_time < coalesce(?1, unixepoch(\'now\', \'+1 second\', \'subsec\'))) SELECT row_num,"cloud_auth_users"."user_id" AS "nested_0.user_id", "cloud_auth_users"."given_name" AS "nested_0.given_name", "cloud_auth_users"."family_name" AS "nested_0.family_name", "cloud_auth_users"."time_zone" AS "nested_0.time_zone", "cloud_auth_users"."language_code" AS "nested_0.language_code", "cloud_auth_users"."create_time" AS "nested_0.create_time", "cloud_auth_users"."update_time" AS "nested_0.update_time", rowed.user_id AS "\$n_0", rowed.user_id AS "\$n_1" FROM cloud_auth_users INNER JOIN rowed ON cloud_auth_users.user_id = rowed.user_id WHERE row_num > ?2 ${generatedorder_by.sql} LIMIT ?3',
1087+
variables: [
1088+
i0.Variable<DateTime>(startTime),
1089+
i0.Variable<int>(offset),
1090+
i0.Variable<int>(limit),
1091+
...generatedorder_by.introducedVariables
1092+
],
1093+
readsFrom: {
1094+
cloudAuthUsers,
1095+
cloudAuthUserEmails,
1096+
cloudAuthUserPhoneNumbers,
1097+
...generatedorder_by.watchedTables,
1098+
}).asyncMap((i0.QueryRow row) async => ListUsersResult(
1099+
rowNum: row.read<int>('row_num'),
1100+
cloudAuthUsers:
1101+
await cloudAuthUsers.mapFromRow(row, tablePrefix: 'nested_0'),
1102+
emails: await customSelect(
1103+
'SELECT * FROM cloud_auth_user_emails WHERE user_id = ?1',
1104+
variables: [
1105+
i0.Variable<String>(row.read('\$n_0'))
1106+
],
1107+
readsFrom: {
1108+
cloudAuthUserEmails,
1109+
}).asyncMap(cloudAuthUserEmails.mapFromRow).get(),
1110+
phoneNumbers: await customSelect(
1111+
'SELECT * FROM cloud_auth_user_phone_numbers WHERE user_id = ?1',
1112+
variables: [
1113+
i0.Variable<String>(row.read('\$n_1'))
1114+
],
1115+
readsFrom: {
1116+
cloudAuthUserPhoneNumbers,
1117+
}).asyncMap(cloudAuthUserPhoneNumbers.mapFromRow).get(),
1118+
));
1119+
}
1120+
10751121
Future<int> deleteUser({required String userId}) {
10761122
return customUpdate(
10771123
'DELETE FROM cloud_auth_users WHERE user_id = ?1',
@@ -1212,6 +1258,22 @@ class CloudAuthUsersDrift extends i4.ModularAccessor {
12121258
i6.CedarDrift get cedarDrift => this.accessor(i6.CedarDrift.new);
12131259
}
12141260

1261+
class ListUsersResult {
1262+
final int rowNum;
1263+
final i1.User cloudAuthUsers;
1264+
final List<i1.Email> emails;
1265+
final List<i1.PhoneNumber> phoneNumbers;
1266+
ListUsersResult({
1267+
required this.rowNum,
1268+
required this.cloudAuthUsers,
1269+
required this.emails,
1270+
required this.phoneNumbers,
1271+
});
1272+
}
1273+
1274+
typedef ListUsers$orderBy = i0.OrderBy Function(
1275+
i2.CloudAuthUsers cloud_auth_users);
1276+
12151277
class LookupUserByEmailResult {
12161278
final i1.User cloudAuthUsers;
12171279
final i1.Email cloudAuthUserEmails;

services/celest_cloud_auth/lib/src/users/users_service.dart

Lines changed: 22 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import 'package:celest_cloud_auth/src/model/order_by.dart';
1717
import 'package:celest_cloud_auth/src/model/page_token.dart';
1818
import 'package:celest_cloud_auth/src/model/route_map.dart';
1919
import 'package:celest_core/celest_core.dart';
20-
import 'package:drift/drift.dart' as drift;
2120
import 'package:drift/drift.dart';
2221
import 'package:meta/meta.dart';
2322
import 'package:shelf/shelf.dart';
@@ -191,74 +190,37 @@ extension type UsersService._(_Deps _deps) implements Object {
191190
final startTime = pageData?.startTime ??
192191
DateTime.timestamp().add(const Duration(seconds: 1));
193192

194-
/*
195-
Roughly, we're trying to construct the query:
196-
197-
WITH rowed AS(
198-
SELECT
199-
ROW_NUMBER() OVER (ORDER BY create_time DESC) AS row_num,
200-
id,
201-
create_time
202-
FROM users
203-
WHERE create_time <= coalesce(:start_time, unixepoch('now', '+1 second', 'subsec'))
204-
)
205-
SELECT
206-
row_num,
207-
users.*
208-
FROM users
209-
JOIN rowed ON users.id = rowed.id
210-
WHERE row_num > coalesce(:page_offset, 0)
211-
ORDER BY :order_by
212-
LIMIT :page_size;
213-
214-
Since we want a dynamic `ORDER BY` clause, we need to construct the query
215-
programmatically. We can't pass `:order_by` as a variable to Drift.
216-
*/
217-
218-
const rowNum = CustomExpression<int>(
219-
'ROW_NUMBER() OVER (ORDER BY create_time DESC)',
220-
);
221-
final rowedQuery = Subquery(
222-
_db.cloudAuthUsers.selectOnly()
223-
..addColumns([
224-
rowNum,
225-
_db.cloudAuthUsers.userId,
226-
_db.cloudAuthUsers.createTime,
227-
])
228-
..where(
229-
_db.cloudAuthUsers.createTime.isSmallerThanValue(startTime),
230-
),
231-
'rowed',
232-
);
233-
234-
final query = _db.cloudAuthUsersDrift.select(_db.cloudAuthUsers).join([
235-
innerJoin(
236-
rowedQuery,
237-
_db.cloudAuthUsers.userId
238-
.equalsExp(rowedQuery.ref(_db.cloudAuthUsers.userId)),
239-
useColumns: false,
240-
),
241-
])
242-
..addColumns([rowedQuery.ref(rowNum)])
243-
..where(
244-
rowedQuery.ref(rowNum).isBiggerThanValue(pageOffset),
245-
);
246-
193+
OrderByClause? orderByClause;
247194
if (orderBy != null) {
248-
final clause = OrderByClause.parse(orderBy);
249-
query.orderBy(clause.toOrderingTerms(_db.cloudAuthUsers).toList());
195+
orderByClause = OrderByClause.parse(orderBy);
250196
}
251197

252-
query.limit(pageSize);
253-
final rows = await query.get();
198+
final rows = await _db.cloudAuthUsersDrift
199+
.listUsers(
200+
startTime: startTime,
201+
offset: pageOffset,
202+
limit: pageSize,
203+
order_by: (tbl) => switch (orderByClause) {
204+
final orderBy? => OrderBy(orderBy.toOrderingTerms(tbl).toList()),
205+
_ => OrderBy([
206+
OrderingTerm(
207+
expression: tbl.createTime,
208+
mode: OrderingMode.desc,
209+
),
210+
]),
211+
},
212+
)
213+
.get();
254214
final users = rows
255-
.map((user) => user.readTable(_db.cloudAuthUsers).toProto())
215+
.map((row) => row.cloudAuthUsers
216+
.copyWith(emails: row.emails, phoneNumbers: row.phoneNumbers)
217+
.toProto())
256218
.toList();
257219
final nextPageToken = rows.isEmpty || rows.length < pageSize
258220
? null
259221
: PageToken(
260222
startTime: startTime,
261-
offset: rows.last.read(rowedQuery.ref(rowNum))!,
223+
offset: rows.last.rowNum,
262224
).encode();
263225
return pb.ListUsersResponse(
264226
users: users,

services/celest_cloud_auth/test/users/users_service_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ void main() {
280280
{
281281
'name': 'users/$userId',
282282
'userId': userId,
283+
'emails': [user.primaryEmail!.toJson()],
283284
'createTime': (Subject<Object?> it) => it
284285
.isA<String>()
285286
.has(DateTime.parse, 'DateTime')

0 commit comments

Comments
 (0)