PHP ODBC not accepting alias on column name with space - php

Basically, I need to get some data out of a SQL Server 2005, using PHP.
Previously we used the mssql_* functions, however due to various server issues we are only now able to use odbc_* functions.
The table has various columns whose names have spaces in them, and before it is suggested.... no I cannot change them as this is a totally separate piece of software in another language and it would break it, I am just getting stats out of it.
Anywho, I had previously been accessing these columns by putting their names in square brackets, e.g. [column name] and it worked fine under the mssql_* functions, however when I do this:
$sql = "select top 1 [assessment name] as AssessmentName from bksb_Assessments";
$result = odbc_exec($db, $sql);
while($row = odbc_fetch_array($result))
{
var_dump($row);
}
It prints the results out as:
'assessment name' => string 'Mathematics E3 Diagnostic' (length=25)
So as you can see it is totally ignoring the alias I've given it and still calling it [assessment name].
But if I run the exact same thing in SQL Server Management Studio Express, it works fine and uses the alias.
I've tried various different combinations, such as quoting the alias, quoting the column, different brackets, etc... but no luck so far.
This isn't a huge issue, as I can change what my script uses to look for "assessment name" in the result array instead of the alias, but it's just a bit annoying that I couldn't work out why this was happening...
Cheers! :)
EDIT:
Actually i don't think the square brackets make a difference, just trying to alias any column isn't working with through php odbc, however I can still do something like CAST(whatever) AS 'alias' and that works fine... just not selecting a column as an alias...? :/

This is by no means a perfect solution, and I would love to learn the proper way to deal with this issue, but in the mean time, add +'' to each column, after the underlying column name, but before the AS alias and you should be able to circumvent this issue.

While attempting to troubleshoot this issue and determine why some of my team's stored procedures exhibited this behavior, I finally discovered why. What's strange is that we were only experiencing this issue with a few stored procedures, while most returned the the aliases as expected.
It appears that declaring at least one variable, whether or not it is being used, will prevent this particular bug from occurring. The variable can be of any type.
If you're just executing a SELECT query without using a stored procedure, use a DECLARE statement to define a variable before or after the statement:
// This works...
DECLARE #ignore TINYINT; SELECT EmployeeID AS employee_id FROM Employee;
// So does this...
SELECT EmployeeID AS employee_id FROM Employee; DECLARE #ignore TINYINT;
Now if you're running into this issue with your stored procedures, you can take the same approach above by defining a variable anywhere in the body.
One other thing I noticed is that if the stored procedure is defined with parameters, if your the parameter gets set or evaluated in something like an if statement, this bug will also not occur. For example,
// This works...
CREATE PROC get_employee(
#employee_id VARCHAR(16) = NULL
)
AS
IF (#employee_id IS NOT NULL)
BEGIN
SELECT FirstName AS first_name FROM Employee WHERE EmployeeID = #employee_id;
END
GO
Using SET:
// So does this...
CREATE PROC get_employee(
#employee_id VARCHAR(16) = NULL,
#ignore TINYINT
)
AS
SET #ignore = NULL;
SELECT FirstName AS first_name FROM Employee WHERE EmployeeID = #employee_id;
GO
I still don't quite understand the cause or nature of the bug; but these workarounds, similar to the type-casting hack above, appear to get around this annoying bug.
It's probably also worth mentioning that we didn't encounter this issue in Ubuntu 14.04 and PHP 5.4. As soon as we upgraded to Ubuntu 18.04 and PHP 7.2, that was when we began experiencing this issue. In both instances, we do have FreeTDS configured to use version 7.1, which is why we're baffled by this particular bug.

#dearsina's approach in fact seems to be one of the few ways to handle this bug.
However, this solution is not applicable to all datatypes. So for me, the only way to cover all needed columns was to CAST() the relevant column (where initially c.State would be of datatype bit):
SELECT
CAST(c.State AS nvarchar) AS 'customer_state'
FROM
customers AS c;
as else you might get an error message similar to
The data types bit and varchar are incompatible in the add operator.
Reference on CASTing large data sets: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/643b6eda-fa03-4ad3-85a1-051f02097c7f/how-much-do-cast-statements-affect-performance?forum=transactsql
Some further finding on this topic is a recently (November 2017) raised bug report at bugs.php.net claiming that the problem is related to FreeTDS: https://bugs.php.net/bug.php?id=75534.
This bug report contains an inofficial (and at least from my end untested) patch:
diff --git a/ext/odbc/php_odbc_includes.h b/ext/odbc/php_odbc_includes.h
index 93e2c96..8451bcd 100644
--- a/ext/odbc/php_odbc_includes.h
+++ b/ext/odbc/php_odbc_includes.h
## -292,18 +292,16 ## void odbc_sql_error(ODBC_SQL_ERROR_PARAMS);
#define PHP_ODBC_SQLCOLATTRIBUTE SQLColAttribute
#define PHP_ODBC_SQLALLOCSTMT(hdbc, phstmt) SQLAllocHandle(SQL_HANDLE_STMT, hdbc, phstmt)
-
-#define PHP_ODBC_SQL_DESC_NAME SQL_DESC_NAME
#else
#define IS_SQL_LONG(x) (x == SQL_LONGVARBINARY || x == SQL_LONGVARCHAR)
#define PHP_ODBC_SQLCOLATTRIBUTE SQLColAttributes
#define PHP_ODBC_SQLALLOCSTMT SQLAllocStmt
-
-#define PHP_ODBC_SQL_DESC_NAME SQL_COLUMN_NAME
#endif
#define IS_SQL_BINARY(x) (x == SQL_BINARY || x == SQL_VARBINARY || x == SQL_LONGVARBINARY)
+#define PHP_ODBC_SQL_DESC_NAME SQL_DESC_LABEL
+
PHP_ODBC_API ZEND_EXTERN_MODULE_GLOBALS(odbc)
#define ODBCG(v) ZEND_MODULE_GLOBALS_ACCESSOR(odbc, v)

Faced similar problem. The 4th parameter of odbc_connect did the trick in my case choosing cursor type SQL_CUR_USE_ODBC.

Related

Undefined table: 7 ERROR: relation "V5TableName" does not exist

I am using an MVC framework (Zend) for my application and I want to find the total size of a table in PostgreSQL (including index). The table name is "V5TableName" - quotes included because table name is case sensitive. I have made sure that there is NO typo involved.
My code to get the table size is shown below:
public function getMyTableSize()
{
$sql = "SELECT pg_size_pretty(pg_total_relation_size( '\"V5TableName\"' ) );";
/* Custom_Db is a custom library in my application which makes the PostgreSQL connection
and queries the database
*/
$tableSize = Custom_Db::query($sql)->fetchColumn();
return $tableSize;
}
When my application calls this function it returns the following error in my logs :
[22-Apr-2020 09:42:37] PID:30849 ERR: SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "V5TableName" does not exist
LINE 1: SELECT pg_size_pretty(pg_total_relation_size( '"V5TableName...
^
query was: SELECT pg_size_pretty(pg_total_relation_size( '"V5TableName"' ) );
If I run this same query in pgAdmin4 it works perfectly fine returning the table size (for instance: 104Mb).
I have tried:
Removing and adding quotes to the table name in the code.
Appending the schema as prefix to the table name (example: 'public."V5TableName"').
NONE of the above seem to work. I am not sure what is going wrong over here.
I also tried to find the total database size in my application (db name: MyDbName - with mixed case spelling) and my query looked something like below:
$sql = "SELECT pg_size_pretty(pg_database_size('MyDbName'))"; // this DID NOT WORK
So I changed it to the one shown below: (it worked)
$sql = "SELECT pg_size_pretty(pg_database_size( current_database() ))"; // this WORKED
I was wondering if there is something similar that could be done to find the table size.
Your query should work. The use of double-quotes seems correct.
SELECT pg_size_pretty(pg_total_relation_size('"V5TableName"'));
First make sure you are connecting to the right database cluster (a.k.a. "server"). It's defined by its data directory, or equally unambiguous by hostname and port number. Read the manual here and here.
Then make sure you are connecting to the right database within that database cluster. A Postgres database cluster consists of 1-n databases. When connecting without specifying the actual database, you end up in the maintenance database named postgres by default. That's the most likely explanation. Check with:
SELECT current_database();
Then check for the right table and schema name:
SELECT * FROM pg_tables
WHERE tablename ~* 'V5TableName'; -- ~* matches case-insensitive
The first riddle should be solved at this point.
Check your DB spelling and possible near-duplicates with:
SELECT datname FROM pg_database;
The call is without double-quotes (like you tried correctly), but requires correct capitalization:
SELECT pg_size_pretty(pg_database_size('MyDbName'));
Note the subtle difference (as documented in the manual):
pg_database_size() takes oid or name. So pass the case-sensitive database name without double-quotes.
pg_total_relation_size() takes regclass. So pass the case-sensitive relation name with double-quotes if you need to preserve capitalization.
pg_database_size() has to differ because there is no dedicated object identifier type for databases (no regdatabase).
The gist of it: avoid double-quoted identifiers in Postgres if at all possible. It makes your life easier.

Mysql Server timing out on specific locate queries

Im programming a search with ZF3 and the DB module.
Everytime i use more than 1 short keyword - like "49" and "am" or "1" and "is" i get this error:
Statement could not be executed (HY000 - 2006 - MySQL server has gone away)
Using longer keywords works perfectly fine as long as i dont use 2 or more short keywords.
The problem only occurs on the live server its working fine on the local test server.
The project table has ~2200 rows with all kind of data the project_search table has 17000 rows with multiple entries for each project , each looking like:
id, projectid, searchtext
The searchtext Column is fulltext. Here the relevant part of the php code:
$sql = new Sql($this->db);
$select = $sql->select(['p'=>'projects']);
if(isset($filter['search'])) {
$keywords = preg_split('/\s+/', trim($filter['search']));
$join = $sql->select('project_search');
$join->columns(['projectid' => new Expression('DISTINCT(projectid)')]);
$join->group("projectid");
foreach($keywords as $keyword) {
$join->having(["LOCATE('$keyword', GROUP_CONCAT(searchtext))"]);
}
$select->join(
["m" => $join],
"m.projectid = p.id",
['projectid'],
\Zend\Db\Sql\Select::JOIN_RIGHT
);
}
Here the resulting Query:
SELECT p.*, m.projectid
FROM projects AS p
INNER JOIN (
SELECT projectid
FROM project_search
GROUP BY projectid
HAVING LOCATE('am', GROUP_CONCAT(searchtext))
AND LOCATE('49', GROUP_CONCAT(searchtext))
) AS m
ON m.projectid = p.id
GROUP BY p.id
ORDER BY createdAt DESC
I rewrote the query using "MATCH(searchtext) AGAINST('$keyword)" and "searchtext LIKE '%keyword%' with the same result.
The problem seems to be with the live mysql server how can i debug this ?
[EDIT]
After noticing that the error only occured in a special view which had other search related queries - each using multiple joins (1 join / keyword) - i merged those queries and the error was gone. The amount of queries seemed to kill the server.
Try refactoring your inner query like so.
SELECT a.projectid
FROM (
SELECT DISTINCT projectid
FROM projectsearch
WHERE searchtext LIKE '%am%'
) a
JOIN (
SELECT DISTINCT projectid
FROM projectsearch
WHERE searchtext LIKE '%49%'
) b ON a.projectid = b.projectid
It should give you back the same set of projectid values as your inner query. It gives each projectid value that has matching searchtext for both search terms, even if those terms show up in different rows of project_search. That's what your query does by searching GROUP_CONCAT() output.
Try creating an index on (searchtext, projectid). The use of column LIKE '%sample' means you won't be able to random-access that index, but the two queries in the join may still be able to scan the index, which is faster than scanning the table. To add that index use this command.
ALTER TABLE project_search ADD INDEX project_search_text (searchtext, projectid);
Try to do this in a MySQL client program (phpmyadmin for example) rather than directly from your php program.
Then, using the MySQL client, test the inner query. See how long it takes. Use EXPLAIN SELECT .... to get an explanation of how MySQL is handling the query.
It's possible your short keywords are returning a ridiculously high number of matches, and somehow overwhelming your system. In that case you can put a LIMIT 1000 clause or some such thing at the end of your inner query. That's not likely, though. 17 kilorows is not a large number.
If that doesn't help your production MySQL server is likely misconfigured or corrupt. If I were you I would call your hosting service tech support, somehow get past the front-line support agent (who won't know anything except "reboot your computer" and other such foolishness), and tell them the exact times you got the "gone away" message. They'll be able to check the logs.
Pro tip: I'm sure you know the pitfalls of using LIKE '%text%' as a search term. It's not scalable because it's not sargable: it can't random access an index. If you can possibly redesign your system, it's worth your time and effort.
You could TRY / CATCH to check if you get a more concrete error:
BEGIN TRY
BEGIN TRANSACTION
--Insert Your Queries Here--
COMMIT
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
IF ##TRANCOUNT > 0
ROLLBACK
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH
Although because you are talking about short words and fulltext it seems to me it must be related to StopWords.
Try running this query from both your dev server and production server and check if there are any differences:
SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;
Also check in my.ini (if that is the config file) text file if these are set to:
ft_stopword_file = ""
ft_min_word_len = 1
As stated in my EDIT the problem wasnt the query from the original Question, but some other queries using the search - parameter as well. Every query had a part like follows :
if(isset($filter['search'])) {
$keywords = preg_split('/\s+/', trim($filter['search']));
$field = 1;
foreach($keywords as $keyword) {
$join = $sql->select('project_search');
$join->columns(["pid$field" => 'projectid']);
$join->where(["LOCATE('$keyword', searchtext)"]);
$join->group("projectid");
$select->join(
["m$field" => $join],
"m$field.pid$field = p.id"
);
$field++;
}
}
This resulted in alot of queries with alot of resultrows killing the mysql server eventually. I merged those Queries into the first and the error was gone.

PostgreSQL: getting error messages from `pg_convert()`

I'm trying to diagnose a problem only found on a production system and I'm trying to improve the error reporting for errors thrown by pg_convert(), but not sure where to go. I tried changing the code in the dev environment to:
$converted_values = pg_convert($cnx, $table, $values);
if ($converted_values === FALSE)
throw new Exception("Failed to insert record: '".pg_last_error($cnx)."' using values: ".var_export($values, true));
But pg_last_error() returned nothing.
Note: I tested the above by trying to insert a null value into a NOT NULL column. In addition to getting no joy from pg_last_error() the log had some "PHP Notice"s in it. The production log didn't have any such notices. Just the empty return value from pg_convert() :(
I'm running PostgreSQL 9.4.6 on Debian 8.3 and PHP 5.6.22.
From what I can tell, trying to get anything done w/pg_convert() is a bust. It's not very reliable in dealing w/different data, and it's inconsistent about putting double quotes around column names. If you're using pg_convert() you'll likely want to convert to prepared statements. What they do is very different, so you might have to make fundamental changes to your code base, but we made the change and it seems to be working for us.
Edit: and one such problem that pg_convert()/pg_query() seemed to be able to roll w/that pg_execute() doesn't, is empty strings assigned to integer columns where NOT NULL isn't set. To fix this (at least for now) I had to:
loop through the array passed to pg_execute()
call pg_meta_data() when I found one w/an empty string
determine if the destination column is an integer (be careful: there are probably a lot of ways in which the type for an integer column can be expressed) then if nulls are allowed at all
overwrite the empty string w/null.
Inserting a NULL into a NOT NULL column will not trigger an error in pg_convert. Errors will result from connection problems, a missing table, or a missing column, or data that cannot be converted from the PHP structure to the type in the database (e.g. a word to a number).

ODBC, PHP and SQL Variables

I have a fairly large, complex chunk of SQL that creates a report for a particular page. This SQL has several variable declarations at the top, which I suspect is giving me problems.
When I get the ODBC result, I have to use odbc_next_result() to get past the empty result sets that seem to be returned with the variables. That seems to be no problem. When I finally get to the "real" result, odbc_num_rows() tells me it has over 12 thousand rows, when in actuality it has 6.
Here is an example of what I'm doing, to give you an idea, without going into details on the class definitions:
$report = $db->execute(Reports::create_sql('sales-report-full'));
while(odbc_num_rows($report) <= 1) {
odbc_next_result($report);
}
echo odbc_num_rows($report);
The SQL looks something like this:
DECLARE #actualYear int = YEAR(GETDATE());
DECLARE #curYear int = YEAR(GETDATE());
IF MONTH(GETDATE()) = 1
SELECT #curYear = #curYear - 1;
DECLARE #lastYear int = #curYear-1;
DECLARE #actualLastYear int = #actualYear-1;
DECLARE #tomorrow datetime = DATEADD(dd, 1, GETDATE());
SELECT * FROM really_big_query
Generally speaking it's always a good idea to start every stored procedure, or batch of commands to be executed, with the set nocount on instruction - which tells SQL Server to supress sending "rows effected" messages to a client application over ODBC (or ADO, etc.). These messages effect performance and cause empty record sets to be created in the result set - which cause additional effort for application developers.
I've also used ODBC drivers that actually error if you forget to suppress these messages - so it has become instinctive for me to type set nocount on as soon as I start writing any new stored procedure.
There are various question/answers relating to this subject, for example What are the advantages and disadvantages of turning NOCOUNT off in SQL Server queries? and SET NOCOUNT ON usage which cover many other aspects of this command.

JSON from SQL Table get double result

first of all i have to tell you that it is my first step on php and JSON.
I decided to use JSON to get value from a customer SQL Table.
I get my results using this script :
mysql_connect($config['mysql_host'],$config['mysql_user'],$config['mysql_pass']);
//select database
#mysql_select_db($config['db_name']) or die( "Unable to select database");
mysql_query('SET CHARACTER SET utf8');
$fet=mysql_query('select * from vehicule');
$json = array();
while($r=mysql_fetch_array($fet)){
$json[] = $r;
}
header('Content-Type: application/json');
echo $json_data=json_encode($json);
Everything is ok, exept that my JSON results looks like :
0 = 462;
1 = "Hyundai ix20 crdi 115 panoramic sunsation";
10 = 1346450400;
11 = "462-Hyundai-ix20-crdi-115-panoramic-sunsation";
12 = 462;
...
id = 462;
kilometrage = 14400;
marque = 4;
modele = 137;
motorisation = 2;
ordre = 462;
prix = 17500;
puissance = 6;
titre = "Hyundai ix20 crdi 115 panoramic sunsation";
url = "462-Hyundai-ix20-crdi-115-panoramic-sunsation";
...
I have result of the table in 2 versions : one with 0:value, 1:value, 2... and the other one using the table key, how can i print only the second one ?
By the way can someone give me link so i can know by what i have to replace mysql which is think out of date ? (i'm a beginner few hours using PHP)
Thank you very much !
You have two different issues happening here. One is outright causing the issue you are seeing, and the other is a bad practice mistake that will leave you wide open for trouble in the long run.
The first issue is the one you're asking about. The mysql_fetch_array function (see the Docs here) expects a minimum of one input (the result input) that you are providing. It also has a second, optional input. That optional input defaults to MYSQL_BOTH, which returns an associative array with the results available both through keys (column names) and their indexes. Which is to say, that if you select the column 'id', you get it's value in both $array[0] and $array['id']. It's duplicated, and thus the JSON process carries over the duplication. You need to provide a second value to the function, either MYSQL_ASSOC to get $array['id'] or MYSQL_NUM to get $array[0].
Your second issue is the choice of functions. You're using the 'raw' mysql functions. These have been depreciated, which is a technical term that means 'these functions are no longer supported, but we've left them in to give you time to fix legacy code'. For legacy, read 'old'. Those functions will be going away soon, and you need to upgrade to a better option -- either the mysqli functions, or the PDO class. I strongly recommend the PDO class, as once you learn it it's easy to learn and has the advantage of being more portable. Whichever set you go with, you need to learn to use prepared statements as both a performance and security issue. Right at the moment, you're working with 'raw' statements which have a history of being very easy to interfere with via what's called an 'injection attack'. You can see a fictionalized example of such an attack here, and there are plenty of articles online about it. These attacks can be incredibly complex and difficult to fight, so using prepared statements (which handle it for you), is strongly recommended. In the specific example you're using here, you don't need to worry about it because you aren't including any user inputs, but it's an important habit to get into.

Categories