2011-01-16 22:51:39 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
2012-03-08 21:46:29 +01:00
|
|
|
* Copyright 2012 Facebook, Inc.
|
2011-01-16 22:51:39 +01:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Format an SQL query. This function behaves like sprintf(), except that
|
|
|
|
* all the normal conversions (like %s) will be properly escaped, and
|
|
|
|
* additional conversions are supported:
|
|
|
|
*
|
|
|
|
* %nd, %ns, %nf
|
|
|
|
* "Nullable" versions of %d, %s and %f. Will produce 'NULL' if the
|
|
|
|
* argument is a strict null.
|
|
|
|
*
|
|
|
|
* %=d, %=s, %=f
|
|
|
|
* "Nullable Test" versions of %d, %s and %f. If you pass a value, you
|
|
|
|
* get "= 3"; if you pass null, you get "IS NULL". For instance, this
|
|
|
|
* will work properly if `hatID' is a nullable column and $hat is null.
|
|
|
|
*
|
|
|
|
* qsprintf($conn, 'WHERE hatID %=d', $hat);
|
|
|
|
*
|
|
|
|
* %Ld, %Ls, %Lf
|
|
|
|
* "List" versions of %d, %s and %f. These are appropriate for use in
|
|
|
|
* an "IN" clause. For example:
|
|
|
|
*
|
|
|
|
* qsprintf($conn, 'WHERE hatID IN(%Ld)', $list_of_hats);
|
|
|
|
*
|
|
|
|
* %T ("Table")
|
|
|
|
* Escapes a table name.
|
|
|
|
*
|
|
|
|
* %C, %LC
|
|
|
|
* Escapes a column name or a list of column names.
|
|
|
|
*
|
|
|
|
* %K ("Comment")
|
|
|
|
* Escapes a comment.
|
|
|
|
*
|
|
|
|
* %Q ("Query Fragment")
|
|
|
|
* Injects a raw query fragment. Extremely dangerous! Not escaped!
|
|
|
|
*
|
|
|
|
* %~ ("Substring")
|
|
|
|
* Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:
|
|
|
|
*
|
|
|
|
* // Find all rows with $search as a substing of `name`.
|
|
|
|
* qsprintf($conn, 'WHERE name LIKE %~', $search);
|
|
|
|
*
|
|
|
|
* See also %> and %<.
|
|
|
|
*
|
|
|
|
* %> ("Prefix")
|
|
|
|
* Escapes a prefix query for a LIKE clause. For example:
|
|
|
|
*
|
|
|
|
* // Find all rows where `name` starts with $prefix.
|
|
|
|
* qsprintf($conn, 'WHERE name LIKE %>', $prefix);
|
|
|
|
*
|
|
|
|
* %< ("Suffix")
|
|
|
|
* Escapes a suffix query for a LIKE clause. For example:
|
|
|
|
*
|
|
|
|
* // Find all rows where `name` ends with $suffix.
|
|
|
|
* qsprintf($conn, 'WHERE name LIKE %<', $suffix);
|
|
|
|
*
|
|
|
|
* @group storage
|
|
|
|
*/
|
2012-04-11 16:04:35 +02:00
|
|
|
function qsprintf(AphrontDatabaseConnection $conn, $pattern/*, ... */) {
|
2011-01-16 22:51:39 +01:00
|
|
|
$args = func_get_args();
|
|
|
|
array_shift($args);
|
|
|
|
return xsprintf('xsprintf_query', $conn, $args);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @group storage
|
|
|
|
*/
|
2012-04-11 16:04:35 +02:00
|
|
|
function vqsprintf(AphrontDatabaseConnection $conn, $pattern, array $argv) {
|
2011-01-16 22:51:39 +01:00
|
|
|
array_unshift($argv, $pattern);
|
|
|
|
return xsprintf('xsprintf_query', $conn, $argv);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* xsprintf() callback for encoding SQL queries. See qsprintf().
|
|
|
|
* @group storage
|
|
|
|
*/
|
|
|
|
function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) {
|
|
|
|
$type = $pattern[$pos];
|
|
|
|
$conn = $userdata;
|
|
|
|
$next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null;
|
|
|
|
|
|
|
|
$nullable = false;
|
|
|
|
$done = false;
|
|
|
|
|
|
|
|
$prefix = '';
|
|
|
|
|
Drive Differential landing page with DifferentialRevisionQuery, simplify UI
Summary:
- Use DifferentialRevisionQuery, not DifferentialRevisionListData, to select
revisions.
- Make UI simpler (I hope?) and more flexible, similar to Maniphest. It now
shows "Active", "Revisions", "Reviews" and "Subscribed" instead of a hodge-podge
of miscellaneous stuff. All now really has all revisions, not just open
revisions.
- Allow views to be filtered and sorted more flexibly.
- Allow anonymous users to use the per-user views, just don't default them
there.
NOTE: This might have performance implications! I need some help evaluating
them.
@nh / @jungejason / @aran, can one of you run some queries agianst FB's corpus?
The "active revisions" view is built much differently now. Before, we issued two
queries:
- SELECT (open revisions you authored that need revision) UNION ALL (open
revisions you are reviewing that need review)
- SELECT (open revisions you authored that need review) UNION ALL (open
revisions you are reviewing that need revision)
These two queries generate the "Action Required" and "Waiting on Others" views,
and are available in P247.
Now, we issue only one query:
- SELECT (open revisions you authored or are reviewing)
Then we divide them into the two tables in PHP. That query is available in P246.
On the secure.phabricator.com data, this new approach seems to be much better
(like, 10x better). But the secure.phabricator.com data isn't very large. Can
someone run it against Facebook's data (using a few heavy-hitting PHIDs, like
ola or something) to make sure it won't cause a regression?
In particular:
- Run the queries and make sure the new version doesn't take too long.
- Run the queries with EXPLAIN and give me the output maybe?
Test Plan:
- Looked at different filters.
- Changed "View User" PHID.
- Changed open/all.
- Changed sort order.
- Ran EXPLAIN / select against secure.phabricator.com corpus.
Reviewers: btrahan, nh, jungejason
Reviewed By: btrahan
CC: cpiro, aran, btrahan, epriestley, jungejason, nh
Maniphest Tasks: T586
Differential Revision: 1186
2011-12-08 00:35:10 +01:00
|
|
|
if (!($conn instanceof AphrontDatabaseConnection)) {
|
|
|
|
throw new Exception("Invalid database connection!");
|
|
|
|
}
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
switch ($type) {
|
|
|
|
case '=': // Nullable test
|
|
|
|
switch ($next) {
|
|
|
|
case 'd':
|
|
|
|
case 'f':
|
|
|
|
case 's':
|
|
|
|
$pattern = substr_replace($pattern, '', $pos, 1);
|
|
|
|
$length = strlen($pattern);
|
|
|
|
$type = 's';
|
|
|
|
if ($value === null) {
|
|
|
|
$value = 'IS NULL';
|
|
|
|
$done = true;
|
|
|
|
} else {
|
|
|
|
$prefix = '= ';
|
|
|
|
$type = $next;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception('Unknown conversion, try %=d, %=s, or %=f.');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'n': // Nullable...
|
|
|
|
switch ($next) {
|
|
|
|
case 'd': // ...integer.
|
|
|
|
case 'f': // ...float.
|
|
|
|
case 's': // ...string.
|
|
|
|
$pattern = substr_replace($pattern, '', $pos, 1);
|
|
|
|
$length = strlen($pattern);
|
|
|
|
$type = $next;
|
|
|
|
$nullable = true;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception('Unknown conversion, try %nd or %ns.');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'L': // List of..
|
|
|
|
_qsprintf_check_type($value, "L{$next}", $pattern);
|
|
|
|
$pattern = substr_replace($pattern, '', $pos, 1);
|
|
|
|
$length = strlen($pattern);
|
|
|
|
$type = 's';
|
|
|
|
$done = true;
|
|
|
|
|
|
|
|
switch ($next) {
|
|
|
|
case 'd': // ...integers.
|
|
|
|
$value = implode(', ', array_map('intval', $value));
|
|
|
|
break;
|
|
|
|
case 's': // ...strings.
|
|
|
|
foreach ($value as $k => $v) {
|
|
|
|
$value[$k] = "'".$conn->escapeString($v)."'";
|
|
|
|
}
|
|
|
|
$value = implode(', ', $value);
|
|
|
|
break;
|
|
|
|
case 'C': // ...columns.
|
|
|
|
foreach ($value as $k => $v) {
|
|
|
|
$value[$k] = $conn->escapeColumnName($v);
|
|
|
|
}
|
|
|
|
$value = implode(', ', $value);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unknown conversion %L{$next}.");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$done) {
|
|
|
|
_qsprintf_check_type($value, $type, $pattern);
|
|
|
|
switch ($type) {
|
|
|
|
case 's': // String
|
|
|
|
if ($nullable && $value === null) {
|
|
|
|
$value = 'NULL';
|
|
|
|
} else {
|
|
|
|
$value = "'".$conn->escapeString($value)."'";
|
|
|
|
}
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'Q': // Query Fragment
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '~': // Like Substring
|
|
|
|
case '>': // Like Prefix
|
|
|
|
case '<': // Like Suffix
|
|
|
|
$value = $conn->escapeStringForLikeClause($value);
|
|
|
|
switch ($type) {
|
|
|
|
case '~': $value = "'%".$value."%'"; break;
|
|
|
|
case '>': $value = "'" .$value."%'"; break;
|
|
|
|
case '<': $value = "'%".$value. "'"; break;
|
|
|
|
}
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'f': // Float
|
|
|
|
if ($nullable && $value === null) {
|
|
|
|
$value = 'NULL';
|
|
|
|
} else {
|
|
|
|
$value = (float)$value;
|
|
|
|
}
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'd': // Integer
|
|
|
|
if ($nullable && $value === null) {
|
|
|
|
$value = 'NULL';
|
|
|
|
} else {
|
|
|
|
$value = (int)$value;
|
|
|
|
}
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'T': // Table
|
|
|
|
case 'C': // Column
|
|
|
|
$value = $conn->escapeColumnName($value);
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'K': // Komment
|
|
|
|
$value = $conn->escapeMultilineComment($value);
|
|
|
|
$type = 's';
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new Exception("Unknown conversion '%{$type}'.");
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($prefix) {
|
|
|
|
$value = $prefix.$value;
|
|
|
|
}
|
|
|
|
$pattern[$pos] = $type;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @group storage
|
|
|
|
*/
|
|
|
|
function _qsprintf_check_type($value, $type, $query) {
|
|
|
|
switch ($type) {
|
|
|
|
case 'Ld': case 'Ls': case 'LC': case 'LA': case 'LO':
|
|
|
|
if (!is_array($value)) {
|
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
|
|
|
"Expected array argument for %{$type} conversion.");
|
|
|
|
}
|
|
|
|
if (empty($value)) {
|
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
|
|
|
"Array for %{$type} conversion is empty.");
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($value as $scalar) {
|
|
|
|
_qsprintf_check_scalar_type($scalar, $type, $query);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
_qsprintf_check_scalar_type($value, $type, $query);
|
2012-03-08 21:46:29 +01:00
|
|
|
break;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @group storage
|
|
|
|
*/
|
|
|
|
function _qsprintf_check_scalar_type($value, $type, $query) {
|
|
|
|
switch ($type) {
|
|
|
|
case 'Q': case 'LC': case 'T': case 'C':
|
|
|
|
if (!is_string($value)) {
|
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
|
|
|
"Expected a string for %{$type} conversion.");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'Ld': case 'd': case 'f':
|
2011-12-14 02:34:41 +01:00
|
|
|
if (!is_null($value) && !is_numeric($value)) {
|
2011-12-04 22:03:12 +01:00
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
2011-12-14 02:34:41 +01:00
|
|
|
"Expected a numeric scalar or null for %{$type} conversion.");
|
2011-12-04 22:03:12 +01:00
|
|
|
}
|
2011-01-16 22:51:39 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'Ls': case 's':
|
|
|
|
case '~': case '>': case '<': case 'K':
|
|
|
|
if (!is_null($value) && !is_scalar($value)) {
|
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
|
|
|
"Expected a scalar or null for %{$type} conversion.");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'LA': case 'LO':
|
|
|
|
if (!is_null($value) && !is_scalar($value) &&
|
|
|
|
!(is_array($value) && !empty($value))) {
|
|
|
|
throw new AphrontQueryParameterException(
|
|
|
|
$query,
|
|
|
|
"Expected a scalar or null or non-empty array for ".
|
|
|
|
"%{$type} conversion.");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unknown conversion '{$type}'.");
|
|
|
|
}
|
|
|
|
}
|