1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-30 10:42:41 +01:00
phorge-phorge/src/storage/qsprintf.php

315 lines
8.4 KiB
PHP
Raw Normal View History

<?php
/*
* Copyright 2012 Facebook, Inc.
*
* 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
*/
function qsprintf(AphrontDatabaseConnection $conn, $pattern/*, ... */) {
$args = func_get_args();
array_shift($args);
return xsprintf('xsprintf_query', $conn, $args);
}
/**
* @group storage
*/
function vqsprintf(AphrontDatabaseConnection $conn, $pattern, array $argv) {
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!");
}
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);
break;
}
}
/**
* @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':
if (!is_null($value) && !is_numeric($value)) {
throw new AphrontQueryParameterException(
$query,
"Expected a numeric scalar or null for %{$type} conversion.");
}
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}'.");
}
}