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
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import 'dart:convert';

import 'package:querya_desktop/core/actions/sql_script_format.dart';
import 'package:querya_desktop/core/database/sql_lexical_scanner.dart';
import 'package:querya_desktop/core/database/sql_limit.dart'
show stripLeadingWhitespaceAndLineComments;
import 'package:querya_desktop/core/extensions/models/sandbox_capabilities.dart';
import 'package:querya_desktop/core/extensions/sandbox/embedded/embedded_sandbox_engine.dart';
import 'package:querya_desktop/core/theme/parser/jsonc_preprocessor.dart';

final _blanks = RegExp(r'[ \t]+');
final _semicolonWithSpace = RegExp(r'\s*;\s*');
final _firstWord = RegExp(r'\S+');

/// Pure-Dart Level-1 engine for JSON / JSONC declarative modules.
///
Expand Down Expand Up @@ -126,41 +135,26 @@ class DeclarativeEmbeddedEngine implements EmbeddedSandboxEngine {
return EmbeddedInvokeResult.failure('sql.format requires args.sql string.');
}

var out = sql.replaceAll(RegExp(r'[ \t]+'), ' ').trim();
out = out.replaceAll(RegExp(r'\s*;\s*'), ';\n');
final upperKeywords = module['uppercaseKeywords'] != false;
if (upperKeywords) {
const keywords = [
'select',
'from',
'where',
'and',
'or',
'join',
'left',
'right',
'inner',
'outer',
'on',
'group',
'by',
'order',
'limit',
'insert',
'into',
'values',
'update',
'set',
'delete',
];
for (final kw in keywords) {
out = out.replaceAllMapped(
RegExp('\\b$kw\\b', caseSensitive: false),
(m) => kw.toUpperCase(),
);
}
// Blanks and `;` are laid out in code only: a literal, quoted identifier
// or comment keeps every character it was written with (#1567).
final out = StringBuffer();
final source = sql.trim();
for (final segment in SqlLexicalScanner.segments(source)) {
final text = source.substring(segment.start, segment.end);
out.write(
segment.kind == SqlSegmentKind.code
? text
.replaceAll(_blanks, ' ')
.replaceAll(_semicolonWithSpace, ';\n')
: text,
);
}
return EmbeddedInvokeResult.success(out);
final laidOut = out.toString();
return EmbeddedInvokeResult.success(
module['uppercaseKeywords'] != false
? formatSqlScript(laidOut)
: laidOut,
);
}

EmbeddedInvokeResult _hintsGenerate(
Expand Down Expand Up @@ -216,7 +210,11 @@ class DeclarativeEmbeddedEngine implements EmbeddedSandboxEngine {
return EmbeddedInvokeResult.failure('sql.parse requires args.sql string.');
}
final trimmed = sql.trim();
final first = trimmed.split(RegExp(r'\s+')).first.toUpperCase();
final first = _firstWord
.firstMatch(stripLeadingWhitespaceAndLineComments(trimmed))
?.group(0)
?.toUpperCase() ??
'';
final dialect = module['dialect'] as String? ?? 'generic';
return EmbeddedInvokeResult.success({
'dialect': dialect,
Expand All @@ -226,11 +224,10 @@ class DeclarativeEmbeddedEngine implements EmbeddedSandboxEngine {
});
}

/// Strips `//` and `/* */` comments then `jsonDecode`s an object.
/// Strips comments and trailing commas (string-aware, see [stripJsonc]),
/// then `jsonDecode`s an object.
static Map<String, dynamic> _parseJsoncObject(String source) {
final withoutBlock = source.replaceAll(RegExp(r'/\*[\s\S]*?\*/'), '');
final withoutLine = withoutBlock.replaceAll(RegExp(r'//[^\n]*'), '');
final decoded = jsonDecode(withoutLine);
final decoded = jsonDecode(stripJsonc(source));
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Module root must be a JSON object.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ void main() {
expect(result.value, 'SELECT * FROM users WHERE id=1;\n');
});

test('sql.format leaves literals, quoted identifiers and comments alone (#1567)',
() async {
final result = await engine.invoke(
const EmbeddedInvokeRequest(
method: EmbeddedInvokeMethod.sqlFormat,
source: '{"kind":"sql.format"}',
args: {
'sql': "select 'a b; order by', \"select\" from t -- from x\n"
'where id=1;',
},
),
);
expect(result.ok, isTrue);
expect(
result.value,
"SELECT 'a b; order by', \"select\" FROM t -- from x\n"
'WHERE id=1;\n',
);
});

test('sql.format can leave keyword case alone', () async {
final result = await engine.invoke(
const EmbeddedInvokeRequest(
method: EmbeddedInvokeMethod.sqlFormat,
source: '{"kind":"sql.format","uppercaseKeywords":false}',
args: {'sql': 'select 1'},
),
);
expect(result.value, 'select 1');
});

test('generates hints filtered by prefix', () async {
const source = '''
{
Expand Down Expand Up @@ -95,6 +126,37 @@ void main() {
expect(ast['statement'], 'UPDATE');
expect(ast['dialect'], 'postgresql');
});

test('sql.parse reads the statement after leading comments (#1567)',
() async {
final result = await engine.invoke(
const EmbeddedInvokeRequest(
method: EmbeddedInvokeMethod.sqlParse,
source: '{"kind":"sql.parse"}',
args: {'sql': '-- note\n/* more */ select 1'},
),
);
expect((result.value! as Map)['statement'], 'SELECT');
});

test('JSONC modules keep // and /* inside strings, allow trailing commas (#1567)',
() async {
final result = await engine.invoke(
const EmbeddedInvokeRequest(
method: EmbeddedInvokeMethod.sqlParse,
source: '''
{
// comment
"kind": "sql.parse",
"homepage": "https://example.com/*not a comment*/",
/* block */ "dialect": "pg",
}''',
args: {'sql': 'select 1'},
),
);
expect(result.ok, isTrue);
expect((result.value! as Map)['dialect'], 'pg');
});
});

group('EmbeddedSandboxRuntime', () {
Expand Down
Loading