Skip to content

Commit 43fc358

Browse files
committed
Add system sql command
1 parent dcba887 commit 43fc358

File tree

4 files changed

+195
-11
lines changed

4 files changed

+195
-11
lines changed

lib/src/commands/admin.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'package:running_on_dart/src/checks.dart';
77
import 'package:running_on_dart/src/modules/poop_name.dart';
88
import 'package:running_on_dart/src/init.dart';
99
import 'package:running_on_dart/src/util/util.dart';
10+
import 'package:running_on_dart/src/services/db.dart';
11+
import 'package:running_on_dart/src/util/sql_result_formatter.dart';
1012

1113
Stream<Member> searchMembers(String disallowedChar, int batchSize, Guild guild) {
1214
return (guild.manager.client as NyxxGateway).gateway.listGuildMembers(
@@ -135,6 +137,29 @@ final admin = ChatGroup(
135137
);
136138
}),
137139
),
140+
ChatCommand(
141+
'sql',
142+
'Execute a SQL query',
143+
id('admin-sql', (
144+
ChatContext context,
145+
@Description('SQL query to execute') String query, [
146+
@Description('Set to false to show response publicly') bool private = true,
147+
]) async {
148+
final db = Injector.appInstance.get<DatabaseService>();
149+
final responseLevel = private ? ResponseLevel.private : ResponseLevel.public;
150+
151+
try {
152+
final result = await db.getConnection().execute(query);
153+
154+
final table = formatSqlResult(result);
155+
final truncated = table.length > 3900 ? '${table.substring(0, 3900)}... (truncated)' : table;
156+
157+
await context.respond(MessageBuilder(content: '```\n$truncated\n```'), level: responseLevel);
158+
} catch (e) {
159+
await context.respond(MessageBuilder(content: 'Error executing query: $e'), level: responseLevel);
160+
}
161+
}),
162+
),
138163
],
139164
),
140165
],

lib/src/services/db.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'dart:async';
2-
import 'dart:io';
32

43
import 'package:logging/logging.dart';
54
import 'package:migent/migent.dart';
@@ -9,20 +8,10 @@ import 'package:running_on_dart/src/settings.dart';
98
import 'package:running_on_dart/src/util/query_builder.dart';
109
import 'package:running_on_dart/src/init.dart';
1110

12-
/// The user to use when connecting to the database.
1311
String user = getEnv('POSTGRES_USER');
14-
15-
/// The password to use when connecting to the database.
16-
// Optional password
1712
String? password = getEnv('POSTGRES_PASSWORD');
18-
19-
/// The name of the database to connect to.
2013
String databaseName = getEnv('POSTGRES_DB');
21-
22-
/// The host name of the database to connect to.
2314
String host = getEnv('DB_HOST', 'db');
24-
25-
/// The port to connect to the database on.
2615
int port = getEnvInt('DB_PORT', 5432);
2716

2817
class DatabaseService implements RequiresInitialization {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'dart:math';
2+
import 'package:postgres/postgres.dart';
3+
4+
const int _maxColumnWidth = 30;
5+
const int _maxRowsToShow = 10;
6+
const String _nullPlaceholder = 'NULL';
7+
8+
String formatSqlResult(Result result) {
9+
return formatTableString(result.toList(), result.schema.columns.map((c) => c.columnName ?? '?').toList());
10+
}
11+
12+
String formatTableString(List<List<dynamic>> rows, List<String> columnNames) {
13+
if (rows.isEmpty) {
14+
return "No results";
15+
}
16+
17+
final truncatedHeaders = columnNames.map(_truncate).toList();
18+
19+
final truncatedRows = rows
20+
.map((row) => row.map((cell) => _truncate(cell?.toString() ?? _nullPlaceholder)).toList())
21+
.toList();
22+
23+
final List<int> columnWidths = _calculateColumnWidths(truncatedHeaders, truncatedRows);
24+
25+
final buffer = StringBuffer();
26+
27+
_appendHeader(buffer, truncatedHeaders, columnWidths);
28+
_appendSeparator(buffer, columnWidths);
29+
_appendRows(buffer, truncatedRows, columnWidths);
30+
31+
if (rows.length > _maxRowsToShow) {
32+
buffer.writeln('... (${rows.length - _maxRowsToShow} more rows)');
33+
}
34+
35+
return buffer.toString();
36+
}
37+
38+
List<int> _calculateColumnWidths(List<String> headers, List<List<String>> rows) {
39+
return List<int>.generate(headers.length, (columnIndex) {
40+
final headerWidth = headers[columnIndex].length;
41+
final cellWidths = rows.map((row) => row[columnIndex].length);
42+
43+
return max(headerWidth, cellWidths.fold(0, max));
44+
});
45+
}
46+
47+
void _appendHeader(StringBuffer buffer, List<String> headers, List<int> columnWidths) {
48+
buffer.write('|');
49+
50+
for (int i = 0; i < headers.length; i++) {
51+
buffer.write(' ${headers[i].padRight(columnWidths[i])} |');
52+
}
53+
54+
buffer.writeln();
55+
}
56+
57+
void _appendSeparator(StringBuffer buffer, List<int> columnWidths) {
58+
buffer.write('|');
59+
60+
for (final width in columnWidths) {
61+
buffer.write('${'-' * (width + 2)}|');
62+
}
63+
64+
buffer.writeln();
65+
}
66+
67+
void _appendRows(StringBuffer buffer, List<List<String>> rows, List<int> columnWidths) {
68+
for (final row in rows.take(_maxRowsToShow)) {
69+
buffer.write('|');
70+
71+
for (int i = 0; i < row.length; i++) {
72+
buffer.write(' ${row[i].padRight(columnWidths[i])} |');
73+
}
74+
75+
buffer.writeln();
76+
}
77+
}
78+
79+
String _truncate(String text) {
80+
if (text.length > _maxColumnWidth) {
81+
return '${text.substring(0, _maxColumnWidth - 3)}...';
82+
}
83+
84+
return text;
85+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:test/test.dart';
2+
import 'package:running_on_dart/src/util/sql_result_formatter.dart';
3+
4+
void main() {
5+
group('formatTableString', () {
6+
test('returns "No results" for empty rows', () {
7+
final result = formatTableString([], ['Column1', 'Column2']);
8+
expect(result, equals('No results'));
9+
});
10+
11+
test('formats basic table correctly', () {
12+
final rows = [
13+
['Alice', 30],
14+
['Bob', 25],
15+
];
16+
final columnNames = ['Name', 'Age'];
17+
18+
final result = formatTableString(rows, columnNames);
19+
20+
expect(
21+
result,
22+
equals(
23+
'| Name | Age |\n'
24+
'|-------|-----|\n'
25+
'| Alice | 30 |\n'
26+
'| Bob | 25 |\n',
27+
),
28+
);
29+
});
30+
31+
test('calculates column widths correctly', () {
32+
final rows = [
33+
['Short', 'Very long value that exceeds max width'],
34+
['Another value', 'Short'],
35+
];
36+
final columnNames = ['Header1', 'Header2'];
37+
38+
final result = formatTableString(rows, columnNames);
39+
40+
// Header1 width = max("Header1".length, "Short".length, "Another value".length) = 13
41+
// Header2 width = max("Header2".length, truncated "Very long value ..." (30), "Short".length) = 30
42+
expect(result, contains('| Header1 | Header2 |'));
43+
expect(result, contains('| Short | Very long value that exceed... |'));
44+
expect(result, contains('| Another value | Short |'));
45+
});
46+
47+
test('truncates long values with ellipsis', () {
48+
final longString = 'This is a very long string that needs to be truncated to fit in the column';
49+
final rows = [
50+
[longString],
51+
];
52+
final columnNames = ['LongColumn'];
53+
54+
final result = formatTableString(rows, columnNames);
55+
56+
expect(result, contains('This is a very long string ...'));
57+
expect(result, contains('| LongColumn |'));
58+
});
59+
60+
test('handles null values as "NULL"', () {
61+
final rows = [
62+
[null, 'Valid'],
63+
['Valid', null],
64+
];
65+
final columnNames = ['Column1', 'Column2'];
66+
67+
final result = formatTableString(rows, columnNames);
68+
69+
expect(result, contains('| NULL | Valid |'));
70+
expect(result, contains('| Valid | NULL |'));
71+
});
72+
73+
test('limits displayed rows to 10 with overflow message', () {
74+
final rows = List.generate(15, (index) => ['Row ${index + 1}']);
75+
final columnNames = ['Data'];
76+
77+
final result = formatTableString(rows, columnNames);
78+
79+
// Should show 10 rows + overflow message
80+
final lines = result.split('\n');
81+
expect(lines.length, 14); // Header + separator + 10 rows + message + empty line
82+
expect(result, contains('... (5 more rows)'));
83+
});
84+
});
85+
}

0 commit comments

Comments
 (0)