New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SELECT: literal variable values are coerced into strings #194
Comments
I thought that maybe my SQL readtable got somewhat wonky, but I reproduced it on a clean Lisp image: CL-USER> (ql:quickload :postmodern)
To load "postmodern":
Load 1 ASDF system:
postmodern
; Loading "postmodern"
.
(:POSTMODERN)
CL-USER> (pomo:connect-toplevel "database" "user" "password" "localhost")
; No value
CL-USER> (pomo:query "SELECT $1;" 1)
(("1"))
1 |
What happens if you actually select an integer from a table? |
This one works fine. SQL/TEST> (pomo:query "SELECT id FROM persona;" :single)
2
1 |
Looking into it. By the way, can you check whether the one line commit I just made fixes your dropped polish character problem? |
Yes, #191 is fixed now. Thank you! |
While I chase this, does the following work for you?
|
Yes, explicit casting works. SQL/TEST> (pomo:query "select $1::integer" 1)
((1)) This would imply that string values are somehow already passed to Postgres as placeholder values. (Keep in mind I'm no specialist in the matter though.) |
Take a look at this. I make a SQL/TEST> (CL-POSTGRES:EXEC-PREPARED POSTMODERN:*DATABASE* "" (LIST "true")
'CL-POSTGRES:LIST-ROW-READER)) This is the stack frame: 0: (CL-POSTGRES::BIND-MESSAGE #<CL+SSL::SSL-STREAM for #<FD-STREAM for "socket 127.0.0.1:47770, peer: 127.0.0.1:5432" {10069F1873}>> "" #(T) #<unavailable argument>)
Locals:
N-PARAMS = 1
NAME = ""
PARAM-FORMATS = #(0)
PARAM-SIZES = #(4)
PARAM-VALUES = #("true")
RESULT-FORMATS = #(T)
SOCKET = #<CL+SSL::SSL-STREAM for #<FD-STREAM for "socket 127.0.0.1:47770, peer: 127.0.0.1:5432" {10069F1873}>>
SB-C::X#2 = 1 However, if I: SQL/TEST> (CL-POSTGRES:EXEC-PREPARED POSTMODERN:*DATABASE* "" (LIST T)
'CL-POSTGRES:LIST-ROW-READER)) This is the stack frame: 0: (CL-POSTGRES::BIND-MESSAGE #<CL+SSL::SSL-STREAM for #<FD-STREAM for "socket 127.0.0.1:47770, peer: 127.0.0.1:5432" {10069F1873}>> "" #(T) #<unavailable argument>)
Locals:
N-PARAMS = 1
NAME = ""
PARAM-FORMATS = #(0)
PARAM-SIZES = #(4)
PARAM-VALUES = #("true")
RESULT-FORMATS = #(T)
SOCKET = #<CL+SSL::SSL-STREAM for #<FD-STREAM for "socket 127.0.0.1:47770, peer: 127.0.0.1:5432" {10069F1873}>>
SB-C::X#2 = 1 The variables in these frames are identical. It seems to me that the symbol And if Postgres receives a string value, then it would explain why Postgres returns a string value. Edit: The |
And this, in turn, would mean that the culprit is at https://github.com/marijnh/Postmodern/blob/master/cl-postgres/sql-string.lisp#L177 which serializes After https://github.com/marijnh/Postmodern/blob/master/cl-postgres/messages.lisp#L167 there is no longer any difference between the symbol |
When sending the parse message, Postmodern omits all parameter types. https://github.com/marijnh/Postmodern/blob/master/cl-postgres/messages.lisp#L100 sends two null bytes for what seems to be the last entry at https://github.com/crate/crate/blob/master/sql/src/main/java/io/crate/protocols/postgres/PostgresWireProtocol.java#L464 This most likely causes Postgres to interpret them however it wants - in this case, as strings. |
When executing 80 ;; #\P
00 00 00 17 ;; length
00 ;; null statement name
83 69 76 69 67 84 32 36 49 00 ;; query, (flex:octets-to-string #(83 69 76 69 67 84 32 36 49)) ;=> "SELECT $1"
00 00 ;; no parameter types |
By means of hacking the 80 ;; #\P
00 00 00 21 ;; new length, 4 bytes longer due to one parameter size
00 ;; null statement name
83 69 76 69 67 84 32 36 49 00 ;; query, (flex:octets-to-string #(83 69 76 69 67 84 32 36 49)) ;=> "SELECT $1"
00 01 ;; one parameter type
00 00 00 16 ;; 16 = boolean And voila! SQL/TEST> (CL-POSTGRES:EXEC-PREPARED POSTMODERN:*DATABASE* "" (LIST T)
'CL-POSTGRES:LIST-ROW-READER)
((T)) Conclusion: we prepare the queries wrong by completely skipping parameter types. The other Postgres wire clients I found, e.g. written in Lua and Java, do not skip that information. |
The question is: why does |
Or, rather: (etypecase param
(string
(set-param 0 (enc-byte-length param) param))
((vector (unsigned-byte 8))
(set-param 1 (length param) param))) And :do (flet ((set-param (format size value)
(setf (aref param-formats i) format
(aref param-sizes i) size
(aref param-values i) value))) In other words, |
No, I misunderstood. Therefore, it is impossible to have generic prepared queries like Wherever possible, Also, it does sound logical that all types should be declared ahead of time - preparing a query sounds like it would involve preparing the result types, too. Sorry for the ranting - I've been trying to document my findings here as I dig deeper and deeper. |
A possible solution I see is to create a new generic function with method signatures similar to |
Confirmed - in the prepared statement of
Therefore the issue is not with how prepared statements work in Postgres. The issue is with how |
My apologies for not being responsive. It has been a bad year. Did you come up with a patch or are we still at the problem stage? Your analysis is much appreciated. |
No problem, hope that things are going steady for you. I haven't produced any patch for that yet. |
I think that the proper solution would be non-trivial.
I think that the above methodology is going to work, though it might be sub-optimal since I am not a Postgres person. Therefore I'd need someone to verify this for me before any implementation attempt is made. |
Continuing in the documenting the issue. In theory we can loop through the parameter arguments and decide which ones to send as binary, passing the arguments through the following chain and then deciding in parse-message what we can send in binary and the oid necessary to tell postgresql what is coming. |- query (postmodern/query) While parse-message should be able to specify the number of parameters which Right now I am not quite in sync with postgresql in the formatting of the messages so I need to solve that before I move forward. FYI Postgresql message format details are found here: https://www.postgresql.org/docs/current/protocol-flow.html Specifically parse-message should be sending a message meeting the following: Parse (F) Int32 String String Int16 Then, for each parameter, there is the following: Int32 BTW, the object id for a four byte integer is 23 but postgresql does not guarantee that the oid for other non-integer data types will not change. What fun. |
Getting there slowly. Sidetracked slightly by a request for scram-sha-256 authentication. |
For those following at home and wondering if this has been dropped, I now have code that works for boolean, short, regular and long integers, single and double floats and text in regular queries, prepared queries (with and without reconnects) and dao queries. Dates are going to be tricky both because common lisp has one format and the simple-date and local-time libraries have their own formats and because postgresql accepts dates in a lot of different text string formats. Does anyone have suggestions on the minimum set of types that should be handled? |
Resolved in this weeks commit allowing parameters to be passed in binary format on a connection by connection basis. |
I put in an integer and received a string.
Is this expected? Are types not preserved across
SELECT
queries?Related to ruricolist/cl-yesql#22
The text was updated successfully, but these errors were encountered: