MDL-77555 reportbuilder: method to ensure unique parameters in SQL.

This commit is contained in:
Paul Holden 2023-03-07 16:48:51 +00:00
parent 5898c3e5dd
commit f2cbacbd24
No known key found for this signature in database
GPG Key ID: A81A96D6045F6164
5 changed files with 78 additions and 11 deletions

View File

@ -101,6 +101,29 @@ class database {
return true;
}
/**
* Replace parameter names within given SQL expression, allowing caller to specify callback to handle their replacement
* primarily to ensure uniqueness when the expression is to be used as part of a larger query
*
* @param string $sql
* @param array $params
* @param callable $callback Method that takes a single string parameter, and returns another string
* @return string
*/
public static function sql_replace_parameter_names(string $sql, array $params, callable $callback): string {
foreach ($params as $param) {
// Pattern to look for param within the SQL.
$pattern = '/:(?<param>' . preg_quote($param) . ')\b/';
$sql = preg_replace_callback($pattern, function(array $matches) use ($callback): string {
return ':' . $callback($matches['param']);
}, $sql);
}
return $sql;
}
/**
* Generate SQL expression for sorting group concatenated fields
*

View File

@ -367,12 +367,12 @@ final class column {
$fields = [];
foreach ($this->fields as $alias => $sql) {
// Ensure params within SQL are prefixed with column index.
foreach ($this->params as $name => $value) {
$sql = preg_replace_callback('/:(?<param>' . preg_quote($name, '\b/') . ')/', function(array $matches): string {
return ':' . $this->unique_param_name($matches['param']);
}, $sql);
}
// Ensure parameter names within SQL are prefixed with column index.
$params = array_keys($this->params);
$sql = database::sql_replace_parameter_names($sql, $params, function(string $param): string {
return $this->unique_param_name($param);
});
$fields[$alias] = [
'sql' => $sql,

View File

@ -139,4 +139,31 @@ class database_test extends advanced_testcase {
$record = $DB->get_record_sql($sql, $params);
$this->assertEquals($admin->id, $record->{$userfieldalias});
}
/**
* Test replacement of parameter names within SQL statements
*/
public function test_sql_replace_parameter_names(): void {
global $DB;
// Predefine parameter names, to ensure they don't overwrite each other.
[$param0, $param1, $param10] = ['rbparam0', 'rbparam1', 'rbparam10'];
$sql = "SELECT :{$param0} AS field0, :{$param1} AS field1, :{$param10} AS field10" . $DB->sql_null_from_clause();
$sql = database::sql_replace_parameter_names($sql, [$param0, $param1, $param10], static function(string $param): string {
return "prefix_{$param}";
});
$record = $DB->get_record_sql($sql, [
"prefix_{$param0}" => 'Zero',
"prefix_{$param1}" => 'One',
"prefix_{$param10}" => 'Ten',
]);
$this->assertEquals((object) [
'field0' => 'Zero',
'field1' => 'One',
'field10' => 'Ten',
], $record);
}
}

View File

@ -173,19 +173,31 @@ class column_test extends advanced_testcase {
* Test adding params to field, and retrieving them
*/
public function test_add_field_with_params(): void {
$param = database::generate_param_name();
[$param0, $param1] = database::generate_param_names(2);
$column = $this->create_column('test')
->set_index(1)
->add_field(":{$param}", 'foo', [$param => 'bar']);
->add_field(":{$param0}", 'foo', [$param0 => 'foo'])
->add_field(":{$param1}", 'bar', [$param1 => 'bar']);
// Select will look like the following: "p<index>_rbparam<counter>", where index is the column index and counter is
// a static value of the report helper class.
$select = $column->get_fields();
preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $select[0], $matches);
$fields = $column->get_fields();
$this->assertCount(2, $fields);
preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $fields[0], $matches);
$this->assertArrayHasKey('paramname', $matches);
$this->assertEquals([$matches['paramname'] => 'bar'], $column->get_params());
$fieldparam0 = $matches['paramname'];
preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_bar/', $fields[1], $matches);
$this->assertArrayHasKey('paramname', $matches);
$fieldparam1 = $matches['paramname'];
// Ensure column parameters have been renamed appropriately.
$this->assertEquals([
$fieldparam0 => 'foo',
$fieldparam1 => 'bar',
], $column->get_params());
}
/**

View File

@ -1,6 +1,11 @@
This file describes API changes in /reportbuilder/*
Information provided here is intended especially for developers.
=== 4.1.3 ===
* New database helper method `sql_replace_parameter_names` to help ensure uniqueness of parameters within an expression (where
that expression can be used multiple times as part of a larger query)
=== 4.1.2 ===
* The schedule helper `create_schedule` method accepts a `$timenow` parameter to use for comparisons against current date