Skip to content
Draft
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
9 changes: 6 additions & 3 deletions flutter_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ class _DittoExampleState extends State<DittoExample> {
// Use the Soft-Delete pattern
// https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern
await _ditto!.store.execute(
"UPDATE tasks SET deleted = true WHERE _id = '${task.id}'",
"UPDATE tasks SET deleted = true WHERE _id = :id",
arguments: {"id": task.id},
);

if (mounted) {
Expand All @@ -209,7 +210,8 @@ class _DittoExampleState extends State<DittoExample> {
title: Text(task.title),
value: task.done,
onChanged: (value) => _ditto!.store.execute(
"UPDATE tasks SET done = $value WHERE _id = '${task.id}'",
"UPDATE tasks SET done = :done WHERE _id = :id",
arguments: {"id": task.id, "done": value},
),
secondary: IconButton(
icon: const Icon(Icons.edit),
Expand All @@ -220,7 +222,8 @@ class _DittoExampleState extends State<DittoExample> {

// https://docs.ditto.live/sdk/latest/crud/update
_ditto!.store.execute(
"UPDATE tasks SET title = '${newTask.title}' where _id = '${task.id}'",
"UPDATE tasks SET title = :title WHERE _id = :id",
arguments: {"id": task.id, "title": newTask.title},
);
},
),
Expand Down
71 changes: 71 additions & 0 deletions flutter_app/test/dql_safety_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Regression test for the parameterized-DQL fix.
//
// Earlier versions of this file built UPDATE statements by interpolating
// user-typed task titles into DQL strings, e.g.:
//
// "UPDATE tasks SET title = '${newTask.title}' where _id = '${task.id}'"
//
// A task titled `'); EVICT FROM tasks WHERE true; --` could break out of
// the quoted value and run arbitrary DQL. The fix switched to named
// parameter binding (`:title`, `:done`, `:id`) with values passed via the
// `arguments:` map.
//
// This test pins the fix as a static-source check: any future change that
// reintroduces Dart string interpolation into the DQL strings in
// `lib/main.dart` fails here loudly. It does not exercise the DQL engine —
// a real round-trip test would need a live `Ditto` instance, which lives in
// `integration_test/`. The check is intentionally coarse and operates on
// the file as text.

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

void main() {
group('main.dart DQL safety', () {
late String source;

setUpAll(() {
source = File('lib/main.dart').readAsStringSync();
});

test('uses named parameter binding for every mutable field', () {
expect(source, contains(':task'), reason: 'INSERT binding missing');
expect(source, contains(':title'), reason: 'updateTitle binding missing');
expect(source, contains(':done'), reason: 'setDone binding missing');
expect(source, contains(':id'),
reason: 'WHERE _id = :id binding missing');
});

test('no Dart string interpolation inside DQL execute() calls', () {
// Find every `_ditto!.store.execute(` (or `.execute(`) call site and
// inspect the string literal that follows up to the next `,` or `)`.
// Forbid `${` and `$identifier` inside those literals.
final calls = RegExp(r'\.execute\(\s*"([^"]*)"').allMatches(source);
expect(
calls.isNotEmpty,
isTrue,
reason: 'No execute() calls found — has main.dart moved?',
);

final shortInterp = RegExp(r'\$[a-zA-Z_{][a-zA-Z0-9_]*');
for (final m in calls) {
final query = m.group(1)!;
expect(
query,
isNot(contains(r'${')),
reason: 'Found `\${` inside DQL: $query — use named binding.',
);
final hit = shortInterp.firstMatch(query);
expect(
hit,
isNull,
reason: hit == null
? ''
: 'Found `${hit.group(0)}` (Dart interpolation) inside DQL: '
'$query — replace with `:name` and pass via `arguments:`.',
);
}
});
});
}
Loading