Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions lib/core/database/sql_limit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,55 @@ int _blockCommentEnd(String sql, int start) {
return sql.length;
}

/// Removes leading whitespace, `--` line comments and `/* */` block comments.
String stripLeadingWhitespaceAndLineComments(String sql) {
var s = sql.trimLeft();
/// The characters `String.trimLeft` removes: Unicode White_Space and the BOM.
bool _isTrimWhitespace(int c) =>
(c >= 0x09 && c <= 0x0D) ||
c == 0x20 ||
c == 0x85 ||
c == 0xA0 ||
c == 0x1680 ||
(c >= 0x2000 && c <= 0x200A) ||
c == 0x2028 ||
c == 0x2029 ||
c == 0x202F ||
c == 0x205F ||
c == 0x3000 ||
c == 0xFEFF;

/// Offset of the first character of [sql] that is not whitespace, a `--` line
/// comment or a `/* */` block comment; `sql.length` when nothing else is there.
///
/// Walks an index, so a header of many comment lines in front of a large script
/// costs one pass and no copies (#1515).
int _skipLeadingCommentsOffset(String sql) {
final length = sql.length;
var i = 0;
while (true) {
if (s.isEmpty) return s;
if (s.startsWith('--')) {
final nl = s.indexOf('\n');
if (nl == -1) return '';
s = s.substring(nl + 1).trimLeft();
while (i < length && _isTrimWhitespace(sql.codeUnitAt(i))) {
i++;
}
if (i >= length) return length;
if (sql.startsWith('--', i)) {
final nl = sql.indexOf('\n', i);
if (nl == -1) return length;
i = nl + 1;
continue;
}
if (s.startsWith('/*')) {
s = s.substring(_blockCommentEnd(s, 0)).trimLeft();
if (sql.startsWith('/*', i)) {
i = _blockCommentEnd(sql, i);
continue;
}
return s;
return i;
}
}

/// Removes leading whitespace, `--` line comments and `/* */` block comments.
String stripLeadingWhitespaceAndLineComments(String sql) {
final start = _skipLeadingCommentsOffset(sql);
if (start >= sql.length) return '';
return start == 0 ? sql : sql.substring(start);
}

final _limitAll = RegExp(r'\bLIMIT\s+ALL\b', caseSensitive: false);
final _limitCount = RegExp(
r'\bLIMIT\s+(\d+)(\s+OFFSET\s+\d+)?',
Expand Down
108 changes: 108 additions & 0 deletions test/core/database/sql_limit_strip_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:math';

import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/database/sql_limit.dart';

/// `stripLeadingWhitespaceAndLineComments` before #1515: one substring + trimLeft
/// per comment, the reference the offset scanner has to match.
String _reference(String sql) {
int blockEnd(String s, int start) {
var depth = 0;
var i = start;
while (i < s.length) {
if (s.startsWith('/*', i)) {
depth++;
i += 2;
} else if (s.startsWith('*/', i)) {
depth--;
i += 2;
if (depth == 0) return i;
} else {
i++;
}
}
return s.length;
}

var s = sql.trimLeft();
while (true) {
if (s.isEmpty) return s;
if (s.startsWith('--')) {
final nl = s.indexOf('\n');
if (nl == -1) return '';
s = s.substring(nl + 1).trimLeft();
continue;
}
if (s.startsWith('/*')) {
s = s.substring(blockEnd(s, 0)).trimLeft();
continue;
}
return s;
}
}

void main() {
group('stripLeadingWhitespaceAndLineComments offset scanner (#1515)', () {
test('agrees with the previous implementation on listed scripts', () {
const cases = <String>[
'',
' ',
'\n\t \r\n',
'SELECT 1',
' SELECT 1',
'-- c\nSELECT 1',
'-- c\r\nSELECT 1',
'-- c\n-- d\n\n -- e\nSELECT 1',
'-- only a comment',
'-- c\n',
'/* c */SELECT 1',
'/* a */ /* b */\n SELECT 1',
'/* a /* nested */ still */ SELECT 1',
'/* unterminated',
'/* a */ -- b\n/* c */ SELECT 1',
'--\nSELECT 1',
'-',
'/',
'SELECT 1 -- trailing\nFROM t',
'  -- c\nSELECT 1',
'\u0085SELECT 1',
'᠎SELECT 1',
'/*/ x */ SELECT 1',
'/**/SELECT 1',
];
for (final sql in cases) {
expect(
stripLeadingWhitespaceAndLineComments(sql),
_reference(sql),
reason: sql,
);
}
});

test('agrees with the previous implementation on random scripts', () {
const pieces = <String>[
'--', '/*', '*/', '\n', '\r\n', ' ', '\t', ' ', '',
'SELECT', '1', 'x', '-', '/', '*',
];
final random = Random(1515);
for (var n = 0; n < 4000; n++) {
final sql = [
for (var k = random.nextInt(24); k > 0; k--)
pieces[random.nextInt(pieces.length)],
].join();
expect(
stripLeadingWhitespaceAndLineComments(sql),
_reference(sql),
reason: sql,
);
}
});

test('a long comment header in front of a large script is one pass', () {
final header = List.generate(500, (i) => '-- header line $i').join('\n');
final body = 'SELECT ${List.filled(20000, 'col').join(', ')} FROM t';
final sql = '$header\n$body';
expect(stripLeadingWhitespaceAndLineComments(sql), body);
});
});
}
Loading