Oracle "ORA-01008: not all variables bound" Error w/ Parameters - sql

This is the first time I've dealt with Oracle, and I'm having a hard time understanding why I'm receiving this error.
I'm using Oracle's ODT.NET w/ C# with the following code in a query's where clause:
WHERE table.Variable1 = :VarA
AND (:VarB IS NULL OR table.Variable2 LIKE '%' || :VarB || '%')
AND (:VarC IS NULL OR table.Variable3 LIKE :VarC || '%')
and I'm adding the parameter values like so:
cmd.Parameters.Add("VarA", "24");
cmd.Parameters.Add("VarB", "test");
cmd.Parameters.Add("VarC", "1234");
When I run this query, the server returns:
ORA-01008: not all variables bound
If I comment out either of the 'AND (....' lines, the query completes successfully.
Why would the query run through alright if I'm only querying with two parameters, but not with three? The error I'm receiving doesn't even make sense

The ODP.Net provider from oracle uses bind by position as default. To change the behavior to bind by name. Set property BindByName to true. Than you can dismiss the double definition of parameters.
using(OracleCommand cmd = con.CreateCommand()) {
...
cmd.BindByName = true;
...
}

It seems daft, but I think when you use the same bind variable twice you have to set it twice:
cmd.Parameters.Add("VarA", "24");
cmd.Parameters.Add("VarB", "test");
cmd.Parameters.Add("VarB", "test");
cmd.Parameters.Add("VarC", "1234");
cmd.Parameters.Add("VarC", "1234");
Certainly that's true with Native Dynamic SQL in PL/SQL:
SQL> begin
2 execute immediate 'select * from emp where ename=:name and ename=:name'
3 using 'KING';
4 end;
5 /
begin
*
ERROR at line 1:
ORA-01008: not all variables bound
SQL> begin
2 execute immediate 'select * from emp where ename=:name and ename=:name'
3 using 'KING', 'KING';
4 end;
5 /
PL/SQL procedure successfully completed.

You might also consider removing the need for duplicated parameter names in your Sql by changing your Sql to
table.Variable2 LIKE '%' || :VarB || '%'
and then getting your client to provide '%' for any value of VarB instead of null. In some ways I think this is more natural.
You could also change the Sql to
table.Variable2 LIKE '%' || IfNull(:VarB, '%') || '%'

Related

Update query with variable

Is it possible to update a variable with a concatenation of variables(columns of VARCHARS2)?
UPDATE ARTICLE
SET DESCRIPTION = (CPV_DESCRIPTION + '/' LEVEL1_DESCRIPTION + LEVEL2_DESCRIPTION+LEVEL3_DESCRIPTION)
WHERE ID_ARTICULO = 209;
UPDATE ARTICLE
SET DESCRIPTION = concat(CPV_DESCRIPTION,'/',LEVEL1_DESCRIPTION,' ',LEVEL2_DESCRIPTION' 'LEVEL3_DESCRIPTION)
WHERE ID_ARTICULO = 209;
Both cases it gives me an error.
As mentioned by #a_horse... concat() function only takes 2 parameters. When you specify more that 2 parameters to be concated, you need to use || operator. Also + is a logical operator in Oracle unlike its used in Java for concatenation. Try this:
UPDATE ARTICLE
SET DESCRIPTION = CPV_DESCRIPTION
|| '/'
||LEVEL1_DESCRIPTION
||' '
||LEVEL2_DESCRIPTION
||' '
||LEVEL3_DESCRIPTION
WHERE ID_ARTICULO = 209;

Delphi 7 SQL Parameter found in Select Statement, but not Insert Statement

I am currently coding a Delphi 7 program that utilises SQL and an Access 2003 database.
The unit receives a 5 digit code from a previous unit, via a public variable (this is frmLogin.sCode). On form activation, the program will execute an SQL query to display the record from tblStudents that matches sCode. This statement uses a ParamByName line and works perfectly.
If no match is found, a message is displayed and the user is left with no option, but to click on the add user button. The user is then prompted to enter all of his details into the program, which are then passed to a class which sets out the SQL Insert Statement. A problem occurs now, however, as a message is displayed stating that Parameter Username is not found. I cannot understand why, as it is found when the Select statement is run. Please could someone help with this?
procedure TfrmProfilePage.FormActivate(Sender: TObject);
begin
//Instantiates the object.
objProfilePage := TProfilePage.Create;
sSQL := objProfilePage.SelectSQL;
ExecuteSQL(sSQl);
end;
procedure TfrmProfilePage.ExecuteSQL(sSQL : String);
begin
With dmTextbookSales do
Begin
dbgrdDisplay.DataSource := dsProfilePage;
qryProfilePage.SQL.Clear;
qryProfilePage.SQL.Add(sSQL);
qryProfilePage.Parameters.ParamByName('Username').Value := frmLogin.sCode;
qryProfilePage.Open;
If qryProfilePage.RecordCount = 0
Then
Begin
ShowMessage('Please click on the "Add Details" button to get started.');
btnChange.Enabled := False;
btnSelling.Enabled := False;
btnBuying.Enabled := False;
End;
End;
end;
procedure TfrmProfilePage.GetValues(VAR sStudentName, sStudentSurname, sCellNumber, sEmailAddress : String; VAR iCurrentGrade : Integer);
begin
ShowMessage('Fields may be left blank, but users wishing to sell textbooks should enter at least one contact field.');
sStudentName := InputBox('Name','Please enter your first name:','');
sStudentSurname := InputBox('Surame','Please enter your surname:','');
iCurrentGrade := StrToInt(InputBox('Current Grade','Please enter your current grade:',''));
sCellNumber := InputBox('Cellphone Number','Please enter your cellphone number:','');
sEmailAddress := InputBox('Email Address','Please enter your email address:','#dainferncollege.co.za');
end;
procedure TfrmProfilePage.btnAddClick(Sender: TObject);
begin
GetValues(sStudentName, sStudentSurname, sCellNumber, sEmailAddress, iCurrentGrade);
sSQL := objProfilePage.InsertSQL;
ExecuteSQL(sSQL);
btnChange.Enabled := True;
btnSelling.Enabled := True;
btnBuying.Enabled := True;
end;
The following code is obtained from the linked class, clsProfilePage:
function TProfilePage.InsertSQL: String;
begin
Result := 'INSERT INTO tblStudents (' + '[StudentID]' + ',' + '[StudentName]' + ',' + '[StudentSurname]' + ',' + '[CurrentGrade]' + ',' + '[CellNumber]' + ',' + '[EmailAddress]' + ') VALUES (' + 'Username' + ',' + QuotedStr(fStudentName) + ',' + QuotedStr(fStudentSurname) + ',' + IntToStr(fCurrentGrade) + ',' + QuotedStr(fCellNumber) + ',' + QuotedStr(fEmailAddress) + ')';
end;
function TProfilePage.SelectSQL: String;
begin
Result := 'SELECT * FROM tblStudents Where StudentID = Username';
end;
Your INSERT statement is wrong. You need to add parameters before you can set parameter values. In Delphi, you do that using : before the parameter name in your SQL statement.
In addition, Open is only used when performing a SELECT. INSERT, UPDATE, and DELETE don't return a rowset, and therefore you must use ExecSQL (the dataset method, not your function with the conflicting name) instead.
(While we're at it, never use RecordCount on a SQL dataset - it requires all rows to be retrieved in order to obtain a count, and it's not relevant when performing anything other than a SELECT anyway. Operations performed by ExecSQL should use RowsAffected instead, which tells you the number of rows that were affected by the operation.
(If you need a count of the number of rows, perform a SELECT COUNT(*) AS NumRecs FROM YourTable WHERE <some condition> instead, and access the NumRecs field using FieldByName.)
Change your function that returns the INSERT statement to something like this (the #13 in Result is a carriage return, which prevents having to manually insert spaces at the end of each line to separate SQL words):
function TProfilePage.InsertSQL: String;
begin
Result := 'INSERT INTO tblStudents ([StudentID],'#13 +
'[StudentName], [StudentSurname],'#13 +
'[CurrentGrade], [CellNumber], [EmailAddress])'#13 +
'VALUES (:Username, :StudentName,'#13 +
':StudentSurname, :CurrentGrade,'#13 +
':CellNumber, :EMailAddress)';
end;
You can then use it with ParamByName, without jumping through all of the hoops with QuotedStr and concatenation:
procedure TfrmProfilePage.AddUser;
begin
with dmTextbookSales do
begin
qryProfilePage.SQL.Text := InsertSQL;
qryProfilePage.Parameters.ParamByName('Username').Value := frmLogin.sCode;
qryProfilePage.Parameters.ParamByName('StudentName').Value := frmLogin.sUserName;
// Repeat for other parameters and values - you have to set every
// single parameter. To skip, set them to a null variant.
// Execute the INSERT statement
qryProfilePage.ExecSQL;
// See if row was inserted, and do whatever.
If qryProfilePage.RowsAffected > 0 then
ShowMessage('User added successfully');
// Perform a SELECT to populate the grid contents with the new
// rows after the update
end;
end;
I'd highly suggest you rethink this code. It's extremely convoluted, and it requires too much to accomplish a simple task (adding a new user).
If it were me, I'd use a separate query that was dedicated only to performing the INSERT operation, where you can set the SQL at design-time, and set the proper types for the parameters in the Object Inspector. Then at runtime, you simply set the parameter values and call ExecSQL on that insert query, and refresh your SELECT query to reflect the new row. It avoids all of the noise and clutter (as well as some unnecessary function calls and convoluted SQL building, opening and closing your SELECT query, etc.).
(It would also allow you to remove that horrific with statement, which leads to hard-to-find bugs and difficult to maintain code.)
You also have some bad linkages between forms, where you're referencing frmLogin (a specific form instance) from a second form. This means that you can never have more than one instance of either form in use at the same time, because you've hard-coded in that reference. I'd rethink that to use either parameters passed in when you create the form, or as properties that are set by the login form when creating the profile page form (or whatever TProfilePage is - your post doesn't say).
The best solution would be to move all of the non-UI related code into a separate unit (such as a TDataModule, which is designed to be used to work with non-visual components such as ADO queries) and remove it from the user-interface related forms anyway, which
Eliminates the coupling between forms, allowing the code to be reused.
Removes the non-visual data related components from cluttering the form and form code.
Separates the business logic (the part not related to interacting with the user) in a separate, distinct location, making it (and the code that uses it) easier to maintain.
Your insert statement is wrong. You need to replace the value for [StudentID].
'INSERT INTO tblStudents ('
+ '[StudentID]' + ','
+ '[StudentName]' + ','
+ '[StudentSurname]' + ','
+ '[CurrentGrade]' + ','
+ '[CellNumber]' + ','
+ '[EmailAddress]' + ')
VALUES ('
+ 'Username' + ',' // <-- UserName will never be replaced by a QuotedStr
// Access is looking for a column with the name
// 'UserName' which can not be found
+ QuotedStr(fStudentName) + ','
+ QuotedStr(fStudentSurname) + ','
+ IntToStr(fCurrentGrade) + ','
+ QuotedStr(fCellNumber) + ','
+ QuotedStr(fEmailAddress) + ')';

In PL/SQL, How do you turn Dynamic SQL into "safe", un-dynamic SQL?

I have some dynamic SQL code, here's a sampple:
v_RUNNING_QUERY VARCHAR2(1000);
BEGIN
v_RUNNING_QUERY := 'SELECT PBA.PTY_ID FROM PARTY.PARTY_BILLING_ACCOUNT PBA
WHERE PBA.BILLING_ACCT_SRC_ID1 = p_BILLING_ACCT_SRC_SYS
AND
PBA.BILLING_ACCT_SRC_SYS_CD = p_PBA.BILLING_ACCT_SRC_SYS_CD;
IF p_BILLING_ACCT_SRC_SYS_ID2 IS NOT NULL
THEN
v_RUNNING_QUERY := v_RUNNING_QUERY || ' AND PBA.BILLING_ACCT_SRC_ID2 = p_BILLING_ACCT_SRC_SYS_ID2';
The issue is that my boss wants me to remove all Dynamic SQL. In place of that, I need to mainly use IF-logic. But he also said that I can use COALESCE . I'm lost ... How does COALESCE help with this ?
You could get the same logic as:
SELECT PBA.PTY_ID
FROM PARTY.PARTY_BILLING_ACCOUNT PBA
WHERE PBA.BILLING_ACCT_SRC_ID1 = p_BILLING_ACCT_SRC_SYS AND
PBA.BILLING_ACCT_SRC_SYS_CD = p_BILLING_ACCT_SRC_SYS_CD AND
(p_BILLING_ACCT_SRC_SYS_ID2 IS NULL OR PBA.BILLING_ACCT_SRC_ID2 = p_BILLING_ACCT_SRC_SYS_ID2)
It does seem that you want some comparison after BILLING_ACCT_SRC_ID2.
EDIT:
You could also phrase this as:
SELECT PBA.PTY_ID
FROM PARTY.PARTY_BILLING_ACCOUNT PBA
WHERE PBA.BILLING_ACCT_SRC_ID1 = p_BILLING_ACCT_SRC_SYS AND
PBA.BILLING_ACCT_SRC_SYS_CD = p_BILLING_ACCT_SRC_SYS_CD AND
PBA.BILLING_ACCT_SRC_ID2 = coalesce(p_BILLING_ACCT_SRC_SYS_ID2, PBA.BILLING_ACCT_SRC_ID2);
This uses coalesce() but the logic doesn't need it.

Using ParseSQL Command for ADO Parameters Cause Invalid Parameter DataType

I have some SQL Command that contains a parameter such:(Note that myID has "int" type in SQL)
vSqlString :='Select * From myTable Where myID= :paramID';
and Use ParseSQL Command to execute this command:
myADOQuery.Parameters.ParseSQL(vSqlString , True);
Now myADOQuery.Parameters.ParamByName('paramID').DataType is smallint Type and it can't accept negative integer values.
I can exactly show to compiler that my Parameter[0].DataType is ftIneteger and it works properly, but what is a good solution for this problem?
My question is asked by Mr. imanShadabi Here: Using TAdoQuery.ParseSql and has resolved by user1008646
Hope this will help.
If you want to create in runtime parameters, you can use something like this:
ADOQuery1.Close;
ADOQuery1.SQL.Text := vSqlString;
ADOQuery1.Parameters.Clear;
ADOQuery1.Parameters.CreateParameter('paramID', ftInteger, pdInput, 10, vIntegerValue);
ADOQuery1.Open;
Or you can concatenate values to the query.
For example:
//For Integer values:
vSqlString: = 'Select * From myTable Where myID =' + IntToStr (vIntegerValue);
//For String values:
vSqlString: = 'Select * From myTable Where myID =' + QuotedStr (vStringValue);
//For float values:
//Be careful with this, usually in a query, the comma is separator values,
//so make sure that the decimal separator is '.'
vDS := DecimalSeparator; //I keep the value it had
DecimalSeparator := '.';
try
ADOQuery1.close;
ADOQuery1.SQL.Text := 'Select * From myTable Where myID='+FloatToStr(vFloatValue);
ADOQuery1.Open;
finally
DecimalSeparator := vDS; //Restore the value that had
end;
The third option is to set the parameters at design time.
But I think this is not what you want.

SQL Query fails on empty result

I have a function that performs a query on a SQL database with an ADO connection, it is simply designed to provide a single result for a database entry that can only have one match for a SELECT type of query (i.e. get me the x value from ID 45, where there is and can only be one ID 45 entry).
The function works fine, until I hit a query that returns no results. The query just hangs, and the application cannot continue. Here is an example query string:
'SELECT Cost FROM MaterialCost ' +
'WHERE MaterialType = ''' + 'MS' +
''' AND Thickness = ''' + '0.250' + '''';
Again this exact string will work fine until I maybe query for something that I know before hand doesn't exist, which should return null or an empty string. Here is the function:
function SelectOneQuery(AQueryString : String; AADOConnectionString : String) : String;
var
ADOQuery: TADOQuery;
begin
//Create empty ADO Query, then connect to connection string
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString:=AADOConnectionString;
ADOQuery.SQL.Clear;
ADOQuery.SQL.Add(AQueryString);
ADOQuery.ExecSQL;
ADOQuery.Open;
//Set first query result and return first result
ADOQuery.First;
if(ADOQuery.Fields.Count > 0) then begin
result:=ADOQuery.Fields[0].Value;
end
else begin
result := '';
end;
end;
I added the fields count thing, but I'm not sure if that's helping at all. Basically, if there are no result, i want result := ''
You have a few problems in your code snippet:
The main problem is that you are checking FieldCount. FieldCount will always be nonzero because it contains the number of columns your query returns, regardless of the fact your query returned records or not. One option is to check RecordCount which represents the number of rows returned but a better option is to check EOF flag.
you are leaking ADOQuery. Always use try/finally blocks to create and cleanup objects.
ExecSQL is used for queries that don't return recordsets (like INSERT and DELETE),
use Open instead
No need to use First after Open
If you use the same query over and over you are better off using parameters, as a bonus your code will be more readable.
Example:
ADOQuery.SQL.Text := 'SELECT Cost FROM MaterialCost WHERE MaterialType = :MaterialType AND Thickness = :Thickness';
ADOQuery.Parameters.ParamByname('MaterialType').Value := 'MS';
ADOQuery.Parameters.ParamByname('Thickness').Value := 0.25;
Your function code should be something like this:
function SelectOneQuery(const AQueryString, AADOConnectionString: string): string;
var
ADOQuery: TADOQuery;
begin
Result := '';
ADOQuery := TADOQuery.Create(nil);
try
ADOQuery.ConnectionString := AADOConnectionString;
ADOQuery.SQL.Text := AQueryString;
ADOQuery.Open;
if not ADOQuery.EOF then
Result := ADOQuery.Fields[0].AsString;
finally
ADOQuery.Free;
end;
end;