How to save an IP address as binary using Eloquent and PostgreSQL? - postgresql

First off, here's the SO question+answer where I got my information - laravel 4 saving ip address to model.
So my table will potentially have millions of row, therefore to keep storage low I opted for option 2 - using the Schema builder's binary() column and converting/storing IPs as binary with the help of Eloquents' accessors/mutators.
Here's my table:
Schema::create('logs', function ( Blueprint $table ) {
$table->increments('id');
$table->binary('ip_address'); // postgresql reports this column as BYTEA
$table->text('route');
$table->text('user_agent');
$table->timestamp('created_at');
});
The first problem I ran into was saving the IP address. I set an accessor/mutator on my model to convert the IP string into binary using inet_pton() and inet_ntop(). Example:
public function getIpAddressAttribute( $ip )
{
return inet_ntop( $ip );
}
public function setIpAddressAttribute( $ip )
{
$this->attributes['ip_address'] = inet_pton( $ip );
}
Trying to save an IP address resulted in the whole request failing - nginx would just return a 502 bad gateway error.
OK. So I figured it had to be something with Eloquent/PostgreSQL not playing well together while passing the binary data.
I did some searching and found the pg_escape_bytea() and pg_unescape_bytea() functions. I updated my model as follows:
public function getIpAddressAttribute( $ip )
{
return inet_ntop(pg_unescape_bytea( $ip ));
}
public function setIpAddressAttribute( $ip )
{
$this->attributes['ip_address'] = pg_escape_bytea(inet_pton( $ip ));
}
Now, I'm able to save an IP address without a hitch (at least, it doesn't throw any errors).
The new problem I'm experiencing is when I try to retrieve and display the IP. pg_unescape_bytea() fails with pg_unescape_bytea() expects parameter 1 to be string, resource given.
Odd. So I dd() $ip in the accessor, the result is resource(4, stream). Is that expected? Or is Eloquent having trouble working with the column type?
I did some more searching and found it's possible that pg_unescape_bytea() is not properly unescaping the data - https://bugs.php.net/bug.php?id=45964.
After much headbanging and hairpulling, it became apparent that I might be approaching this problem from the wrong direction, and need some fresh perspective.
So, what am I doing wrong? Should I be using Postgres' BIT VARYING instead of BYTEA by altering the column type --
DB::statement("ALTER TABLE logs ALTER COLUMN ip_address TYPE BIT VARYING(16) USING CAST(ip_address AS BIT VARYING(16))");`
-- Or am I merely misusing pg_escape_bytea / pg_unescape_bytea?
All help is appreciated!

Like already said in the comments to your question: in your specific case you should use the corresponding PostgreSQL data type and handling will be much easier. Compared to MySQL you will have a lot of other types in PostgreSQL (like JSON), there is a PostgreSQL data type overview page for further reference.
That said, other people could stumble upon a similar problem with bytea fields. The reason why you got Resource instead of string was that PostgreSQL treats bytea fields as streams. A very naïve approach would be to first get the stream and then to return the data:
public function getDataAttribute($value)
{
// This will kill your server under high load with large values.
$data = fgets($value);
return pg_unescape_bytea($data);
}
You can imagine that this could be a problem where multiple people try to get big files (currently hundreds of MiB or a couple of GiB) where large data objects would need a lot of memory on the server (this could even get a problem on mobile devices without swap). In this case you should work with streams on the server and the client and just fetch the data on the client you really need.

Related

Why are identical SQL calls behaving differently?

I'm working on a web app in Rust. I'm using Tokio Postgres, Rocket and Tera (this may be relevant).
I'm using the following to connect to my DB which doesn't fail in either case.
(sql_cli, connection) = match tokio_postgres::connect("postgresql://postgres:*my_password*#localhost:8127/*AppName*", NoTls).await{
Ok((sql_cli, connection)) => (sql_cli, connection),
Err(e) => return Err(Redirect::to(uri!(error_display(MyError::new("Failed to make SQLClient").details)))),
};
My query is as follows. I keep my queries in a separate file (I'm self taught and find that easier).
let query= sql_cli.query(mycharactersquery::get_characters(user_id).as_str(), &[]).await.unwrap();
The get characters is as follows. It takes a user ID and should return the characters that they have made in the past.
pub fn get_characters(user_id: i16) -> String {
format!("SELECT * FROM player_characters WHERE user_id = {} ORDER BY char_id ASC;", user_id)
}
In my main file, I have one GET which is /mycharacters/<user_id> which works. This GET returns an HTML file. I have another GET which is /<user_id> which returns a Tera template. The first works fine and loads the characters, the second doesn't: it just loads indefinitely. I initially thought this was to do my lack of familiarity with Tera.
After some troubleshooting, I put some printouts in my code, the one before and after the SQL call work in /mycharacters/<user_id>, but only the one before writes to the terminal in /<user_id>. This makes me think that Tera isn't the issue as it isn't making it past the SQL call.
I've found exactly where it is going wrong, but I don't know why as it isn't giving an error.
Could someone please let me know if there is something obvious that I am missing or provide some assistance?
P.S. The database only has 3 columns, so an actual timeout isn't the cause.
I expected both of these SQL calls to function as I am connected to my database properly and the call is copied from the working call.

SQL::Abstract Type Cast Column

Using SQL::Abstract I need to type cast an IP column to TEXT in order to be able to search using LIKE.
I only found a "hacky" way to achieve it with:
where( { ip => { '::TEXT LIKE' => $myParameter } } )
Which generates
WHERE ( "ip" ::TEXT LIKE ? )
Question is: Is there a less hacky or official way to achieve this?
Questions are not:
Will the performance be poor?
Should I use a TEXT column instead of an IP column?
Are searches using CIDR a better alternative
The issue was in Mojo::Pg which adds a quote_char of " to the SQL::Abstract object.
When I set this to the empty string, this will work as expected:
where( { 'ip::TEXT' => { 'LIKE' => $myParameter } } )
But, to be complete, I had to use
where( { 'host(ip)' => { 'LIKE' => $myParameter } } )
because ::TEXT will give the IP with an appended /32.
I think you mix a lot of things in your question. You make it sound like it is an SQL::Abstract issue, when your real issue is with the underlying SQL itself.
First of all, I would personally avoid using SQL::Abstract in most cases (it is sometimes VERY slow to create complex queries and you can't be sure of its output) and definitely in cases like this where you want something non-standard.
So, moving on, I am not sure what you mean by IP type, from your postgres tag and the mention of CIDR I suspect you mean the inet type? If so, the equivalent of LIKE is to use subnet masks, which is basically the whole reason to use inet instead of a text/varchar field. For 192.168.* for example you would do something like below using the subnet slash notation:
SELECT * FROM users WHERE ip << inet '192.168.0.0/16'
If you don't want to treat IPs as actual IPs and take advantage of things like above, but instead you want to treat them like text (e.g. you want to search 192.16% with that producing 192.168.* results but along with 192.16.* etc which is not done with subnet masks), then you either use a text type in the first place, or as you said convert on the fly to use LIKE directly:
SELECT * FROM users WHERE TEXT(ip) LIKE '192.168.%'
There is a performance penalty over using the subnet masks, but whether that may be an issue depends on your data of course.
Note cidr works similar to inet, it won't help you with a LIKE.

operator does not exist: # timestamp without time zone

In a parameterized query issued from c# code to PostgreSQL 10.14 via dotConnect 7.7.832 .NET connector, I select either a parameter value or the local timestamp, if the parameter is NULL:
using (var cmd = new PgSqlCommand("select COALESCE(#eventTime, LOCALTIMESTAMP)", connection)
When executed, this statement throws the error in subject. If I comment out the corresponding parameter
cmd.Parameters.Add("#eventTime", PgSqlType.TimeStamp).Value = DateTime.Now;
and hardcode
using (var cmd = new PgSqlCommand("select COALESCE('11/6/2020 2:36:58 PM', LOCALTIMESTAMP)", connection)
or if I cast the parameter
using (var cmd = new PgSqlCommand("select COALESCE(cast(#eventTime as timestamp without time zone), LOCALTIMESTAMP)", connection)
then it works. Can anyone explain what # operator in the error is referring to and why the error?
In the case that doesn't work, your .Net connection library seems to be passing an SQL command containing a literal # to the database, rather than substituting it. The database assumes you are trying to use # as a user defined operator, as it doesn't know what else it could possibly be. But no such operator has been defined.
Why is it doing that? I have no idea. That is a question about your .Net connection library, not about PostgreSQL itself, so you might want to add tag.
The error message you get from the database should include the text of the query it received (as opposed to the text you think it was sent) and it is often useful to see that in situations like this. If that text is not present in the client's error message (some connection libraries do not faithfully pass this info along) you should be able to pull it directly from the PostgreSQL server's log file.

Intersystems Cache - Maintaining Object Code to ensure Data is Compliant with Object Definition

I am new to using intersytems cache and face an issue where I am querying data stored in cache, exposed by classes which do not seem to accurately represent the data in the underlying system. The data stored in the globals is almost always larger than what is defined in the object code.
As such I get errors like the one below very frequently.
Msg 7347, Level 16, State 1, Line 2
OLE DB provider 'MSDASQL' for linked server 'cache' returned data that does not match expected data length for column '[cache]..[namespace].[tablename].columname'. The (maximum) expected data length is 5, while the returned data length is 6.
Does anyone have any experience with implementing some type of quality process to ensure that the object definitions (sql mappings) are maintained in such away that they can accomodate the data which is being persisted in the globals?
Property columname As %String(MAXLEN = 5, TRUNCATE = 1) [ Required, SqlColumnNumber = 2, SqlFieldName = columname ];
In this particular example the system has the column defined with a max len of 5, however the data stored in the system is 6 characters long.
How can I proactively monitor and repair such situations.
/*
I did not create these object definitions in cache
*/
It's not completely clear what "monitor and repair" would mean for you, but:
How much control do you have over the database side? Cache runs code for a data-type on converting from a global to ODBC using the LogicalToODBC method of the data-type class. If you change the property types from %String to your own class, AppropriatelyNamedString, then you can override that method to automatically truncate. If that's what you want to do. It is possible to change all the %String property types programatically using the %Library.CompiledClass class.
It is also possible to run code within Cache to find records with properties that are above the (somewhat theoretical) maximum length. This obviously would require full table scans. It is even possible to expose that code as a stored procedure.
Again, I don't know what exactly you are trying to do, but those are some options. They probably do require getting deeper into the Cache side than you would prefer.
As far as preventing the bad data in the first place, there is no general answer. Cache allows programmers to directly write to the globals, bypassing any object or table definitions. If that is happening, the code doing so must be fixed directly.
Edit: Here is code that might work in detecting bad data. It might not work if you are doing cetain funny stuff, but it worked for me. It's kind of ugly because I didn't want to break it up into methods or tags. This is meant to run from a command prompt, so it would have to be modified for your purposes probably.
{
S ClassQuery=##CLASS(%ResultSet).%New("%Dictionary.ClassDefinition:SubclassOf")
I 'ClassQuery.Execute("%Library.Persistent") b q
While ClassQuery.Next(.sc) {
If $$$ISERR(sc) b Quit
S ClassName=ClassQuery.Data("Name")
I $E(ClassName)="%" continue
S OneClassQuery=##CLASS(%ResultSet).%New(ClassName_":Extent")
I '$IsObject(OneClassQuery) continue //may not exist
try {
I 'OneClassQuery.Execute() D OneClassQuery.Close() continue
}
catch
{
D OneClassQuery.Close()
continue
}
S PropertyQuery=##CLASS(%ResultSet).%New("%Dictionary.PropertyDefinition:Summary")
K Properties
s sc=PropertyQuery.Execute(ClassName) I 'sc D PropertyQuery.Close() continue
While PropertyQuery.Next()
{
s PropertyName=$G(PropertyQuery.Data("Name"))
S PropertyDefinition=""
S PropertyDefinition=##CLASS(%Dictionary.PropertyDefinition).%OpenId(ClassName_"||"_PropertyName)
I '$IsObject(PropertyDefinition) continue
I PropertyDefinition.Private continue
I PropertyDefinition.SqlFieldName=""
{
S Properties(PropertyName)=PropertyName
}
else
{
I PropertyName'="" S Properties(PropertyDefinition.SqlFieldName)=PropertyName
}
}
D PropertyQuery.Close()
I '$D(Properties) continue
While OneClassQuery.Next(.sc2) {
B:'sc2
S ID=OneClassQuery.Data("ID")
Set OneRowQuery=##class(%ResultSet).%New("%DynamicQuery:SQL")
S sc=OneRowQuery.Prepare("Select * FROM "_ClassName_" WHERE ID=?") continue:'sc
S sc=OneRowQuery.Execute(ID) continue:'sc
I 'OneRowQuery.Next() D OneRowQuery.Close() continue
S PropertyName=""
F S PropertyName=$O(Properties(PropertyName)) Q:PropertyName="" d
. S PropertyValue=$G(OneRowQuery.Data(PropertyName))
. I PropertyValue'="" D
.. S PropertyIsValid=$ZOBJClassMETHOD(ClassName,Properties(PropertyName)_"IsValid",PropertyValue)
.. I 'PropertyIsValid W !,ClassName,":",ID,":",PropertyName," has invalid value of "_PropertyValue
.. //I PropertyIsValid W !,ClassName,":",ID,":",PropertyName," has VALID value of "_PropertyValue
D OneRowQuery.Close()
}
D OneClassQuery.Close()
}
D ClassQuery.Close()
}
The simplest solution is to increase the MAXLEN parameter to 6 or larger. Caché only enforces MAXLEN and TRUNCATE when saving. Within other Caché code this is usually fine, but unfortunately ODBC clients tend to expect this to be enforced more strictly. The other option is to write your SQL like SELECT LEFT(columnname, 5)...
The simplest solution which I use for all Integration Services Packages, for example is to create a query that casts all nvarchar or char data to the correct length. In this way, my data never fails for truncation.
Optional:
First run a query like: SELECT Max(datalength(mycolumnName)) from cachenamespace.tablename.mycolumnName
Your new query : SELECT cast(mycolumnname as varchar(6) ) as mycolumnname,
convert(varchar(8000), memo_field) AS memo_field
from cachenamespace.tablename.mycolumnName
Your pain of getting the data will be lessened but not eliminated.
If you use any type of oledb provider, or if you use an OPENQUERY in SQL Server,
the casts must occur in the query sent to Intersystems CACHE db, not in the the outer query that retrieves data from the inner OPENQUERY.

Stop Zend_Db from quoting Sybase BIT datatype field values

I'm using Pdo_Mssql adapter against a Sybase database and working around issues encountered. One pesky issue remaining is Zend_Db's instance on quoting BIT field values. When running the following for an insert:
$row = $this->createRow();
...
$row->MyBitField = $data['MyBitField'];
...
$row->save();
FreeTDS log output shows:
dbutil.c:87:msgno 257: "Implicit conversion from datatype 'VARCHAR' to 'BIT' is not allowed. Use the CONVERT function to run this query.
I've tried casting values as int and bool, but this seems to be a table metadata problem, not a data type problem with input.
Fortunately, Zend_Db_Expr works nicely. The following works, but I'd like to be database server agnostic.
$row->MyBitField = new Zend_Db_Expr("CONVERT(BIT, {$data['MyBitField']})");
I've verified that the describeTable() is returning BIT for the field. Any ideas on how to get ZF to stop quoting MS SQL/Sybase BIT fields?
You can simply try this (works for mysql bit type):
$row->MyBitField = new Zend_Db_Expr($data['MyBitField']);