(For readers who don't use Delphi: Although the following is couched in terms of Delphi coding, my actual technical question isn't Delphi-specific, but is about how to find out how the Sql Server will "understand" a TransactSql batch submitted to it. A "TAdoQuery" is a Delphi class which basically wraps an ADO Command and a RecordSet and submits a TSql batch to a Sql Server. Usually, using the TAdoQuery, the batch is a single statement, but my q is specifically concerned with the possibility that the batch may contain more than one statement.)
Suppose I have a TAdoQuery whose Sql.Text contains a TransactSql batch comprising
one or more statements S1[...Sn].
What I'm trying to do is to find out without executing the batch whether a) the first (or only) statement, S1, in the batch will return a result set (even if empty) e.g by dint of it being a SELECT statement or an invocation of a stored procedure or table function, or whatever, AND b) how many statements the server thinks there are in the batch.
Regular users of Delphi's TAdoQuery will know that it's easy but slightly messy to test whether the first (or only) statement in a batch returns a result set just by calling TAdoQuery.Open. If it does, then it retrieves that result set, but if doesn't, then calling .Open will provoke an exception.
So, instead, I do something like this:
type
TMyDataSet = class(TDataSet);
procedure TForm1.Button1Click(Sender: TObject);
begin
if AdoQuery1.Active then
AdoQuery1.Close;
AdoQuery1.FieldDefs.Clear;
TMyDataSet(AdoQuery1).OpenCursor(True);
AdoQuery1.FieldDefList.Update;
//AdoQuery1.FieldList.Update;
//Listbox1.Items.Assign(AdoQuery1.FieldList);
end;
The call to .OpenCursor with its InfoQuery param set to true causes the AdoQuery's FieldDefs
to be populated iff the first statement in its Sql would return a result set, but, unlike calling .Open, it will not cause the batch to be executed.
So far, so good. Here's my question:
How (via the AdoQuery or otherwise) do I get the Sql Server to tell me how many
statements it thinks the batch contains? (I think I may have stumbled on a way (subject to a lot more testing), but am interested in whether anyone knows an "official" technique for doing this.)
Btw, for now I'm using an antique (Sql Server 2000!) server via its OleDB driver for Sql Server.
You can use the SET SHOWPLAN_ALL function to analyze the statement in SQL server instead of executing the query. Please note that you can't use this functionality with TADOQuery but only with a TADOCommand object (Like TADOConnection.Execute).
test table:
USE [TestCustomer]
GO
CREATE TABLE [dbo].[Tbl_test](
[Id] [int] NULL,
[col1] [varchar](50) NULL
) ON [PRIMARY]
GO
small demo program:
program SO27007086;
{$APPTYPE CONSOLE}
uses
ActiveX,
Db,
AdoDb,
SysUtils;
var
DbConn : TADOConnection;
function GetNumberOfStatements(SQLQuery: String): Integer;
var
Rs : _RecordSet;
LastId : Integer;
begin
Result := 0;
LastId := -1;
DbConn.Execute('SET SHOWPLAN_ALL ON');
Rs := DbConn.Execute(SQLQuery, cmdText, []);
while not Rs.EOF do
begin
if Rs.Fields['StmtId'].Value <> LastId then
begin
Inc(Result);
LastId := Rs.Fields['StmtId'].Value;
end;
if Rs.Fields['Parent'].Value = 0 then
Writeln(Rs.Fields['Type'].Value);
Rs.MoveNext;
end;
DbConn.Execute('SET SHOWPLAN_ALL OFF');
end;
begin
try
try
CoInitialize(nil);
DbConn := TADOConnection.Create(nil);
try
DbConn.ConnectionString := 'Provider=SQLOLEDB;Integrated Security=SSPI;Initial Catalog=TestCustomer;Data Source=localhost\SQLEXPRESS;MARS Connection=True;';
DbConn.Connected := True;
Writeln(GetNumberOfStatements('SELECT * FROM Tbl_test'));
Writeln(GetNumberOfStatements('SELECT * FROM Tbl_test DELETE FROM Tbl_test WHERE 1 = 2'));
Writeln(GetNumberOfStatements('SELECT * FROM Tbl_test INSERT INTO Tbl_Test (Id, Col1) VALUES (3, ''c''),(4, ''d'')'#13#10'DELETE FROM Tbl_test WHERE 1 = 2'));
finally
DbConn.Free;
end;
finally
CoUninitialize;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Output:
SELECT
1
SELECT
DELETE
2
SELECT
INSERT
DELETE
3
Related
Hi i have one doubt in snowflake how to write multiple update stments using stored procedure.
i have tried like below
create or replace procedure sp_multipleupdate()
returns table()
lANGUAGE sql
as
$$
declare res rsultset(
update TEST.PUBLIC.DEPT set Dname='PM' where deptid=10;
update TEST.PUBLIC.emp set name='veavi' where deptno=20;
update TEST.PUBLIC.loc set locname='del' where id=5;
)
begin
return table(res);
end;
$$;
getting error :
000006 (0A000): Multiple SQL statements in a single API call are not supported; use one API call per statement instead.
Syntax error: unexpected '('. (line 2)
please let me know how to write query to achive this task in snowflake server .
Multiple SQL statements inside the resultset are not supported.
Rather than writing the UPDATE statements like that I would create a more generic procedure and pass arguments to it, so maybe split the above one in 3 procedures since these UPDATE statements are for different tables.
Here is a sample of a generic stored procedure:
create or replace procedure find_invoice_by_id_via_execute_immediate(id varchar)
returns table (id integer, price number(12,2))
language sql
as
declare
select_statement varchar;
res resultset;
begin
select_statement := 'SELECT * FROM invoices WHERE id = ' || id;
res := (execute immediate :select_statement);
return table(res);
end;
You can read more here.
I've got a small problem that I could not find an answer to. I need to create/alter some procedures from Delphi.
So this is my code that take the code from a file and tries to execute it.
procedure TfrmMainApp.actRulezaScriptExecute(Sender: TObject);
var
j: Int32;
sql: string;
commandFile: TextFile;
Linie: string;
affRows: Int32;
err: string;
begin
for j := 0 to filesToExecute.Count - 1 do
begin
sql := 'USE ' + DBName + #10#13;
sql := sql + ' GO ' + #10#13;
AssignFile(commandFile, filesToExecute[j]);
Reset(commandFile);
while not EOF(commandFile) do
begin
Readln(commandFile, Linie);
sql := sql + #10#13 + Linie;
end;
dmMainScriptRun.ExecuteCommand(sql, err, affRows);
if err <> '' then
break;
Memo1.Lines.Add('Affected rows:' + IntToStr(affRows));
end;
end;
function TdmMainScriptRun.ExecuteCommand(sqlCommand: string; var err: string;
var affRows: Int32): Boolean;
begin
err := '';
try
with cmd do
begin
CommandText := sqlCommand;
Execute(affRows, EmptyParam);
end;
except
on E: Exception do
begin
err := E.Message;
end;
end;
end;
So my file looks like this
CREATE PROCEDURE sp_TestSebi
AS
print ('testSebi')
My command looks like this ( it was taken from the SQL Server Profiler )
USE Test
GO
CREATE PROCEDURE sp_TestSebi
AS
print ('testSebi')
Executin this command returns err Incorrect syntax near 'GO' running the script without the GO statement return err CREATE/ALTER PROCEDURE' must be the first statement in a query batch because of the USE clause.
Is there a way I can create a procedure from Delphi? I need the use statement because i'm trying to execute a script on multiple databases.
The way you are attempting this can be improved in several ways.
Firstly, try this:
Create a new VCL project
Drop a TAdoConnection and a TAdoQuery on the form. Also drop a TButton on it.
Connect the TAdoQuery to the TAdoConnection.
Set the TAdoConnection to connect to your Sql Server
in the code below, modify the scUse constant to refer to your target database
and scCreateView to refer to a non-existing view and a valid table in your db. This is to ensure that the Create View will not fail because the view already exists or the table does not exist.
Run the code and you should get a complaint that the Create View refers to an invalid object name, because the AdoConnection isn't connected to your target database when the Create View executes.
Then change KeepConnection to True and retest. This time the view should be successfully created.
const
scUse ='use m4common';
scCreateView = 'create view vwtest as select * from btnames';
procedure TForm1.Button1Click(Sender: TObject);
begin
AdoConnection1.DefaultDatabase := 'Master';
// The point of changing the default database in the line above
// is to ensure for demo purposes that the 'Use' statement
// is necessary to change to the correct db.
AdoConnection1.KeepConnection := False; // retest with this set to True
AdoQuery1.SQL.Text := scUse;
AdoQuery1.ExecSQL;
AdoQuery1.SQL.Text := scCreateView;
AdoQuery1.ExecSQL;
end;
So, the point of KeepConnection is to allow you to execute two or more Sql batches in the same connection context and to satisfy the server that the Create View statement (or similar) can be the first statement in the batch, and at the same time, the database to which the Create View applies is the same one as you "USEd" in the previous batch.
Secondly, your AssignFile ... while not Eof is unnecessarily long-winded and error-prone. Try something like this instead:
var
TL : TStringList;
begin
TL := TStringList.Create;
try
for j := 0 to filesToExecute.Count - 1 do
begin
sql := 'USE ' + DBName + #13#10;
sql := sql + ' GO ' + #13#10;
TL.BeginUpdate; // for better performance with longer scripts
TL.LoadFromFile(filesToExecute[j]);
TL.Insert(0, sql);
TL.EndUpdate;
// execute the Sql here etc
end;
finally
TL.Free;
end;
end;
Note that I've reversed the order of your #10 and #13 so that it is correct.
The other point is that as the Lines property of your Memo already has a LoadFromFile method, you don't really need my temporary TStringList, TL, because you could do the load into your memo (though you might prefer to keep the two uses separate).
You can drop the connection each time and specify the USE in the params. In fact if you are clever you can establish one connection per db and parallelize the whole thing.
I experience difficulties with the Delphi/ADO error handling when executing an SQL script containing more than one INSERT, UPDATE,... statement. Only when the first SQL statement of the script fails, I get an exception in Delphi. If the first statement passes, there will be no exception in Delphi, whatever happens further in the script.
This is the Delphi code I use:
var
DataSet: TADOQuery;
begin
...
try
DataSet.Close;
DataSet.ParamCheck := true;
DataSet.SQL.LoadFromFile(FileName);
DataSet.Prepared := true;
try
DataSet.ExecSQL;
finally
DataSet.Close;
end;
except
on E: Exception do
Logging.AddText(E.ClassName + ' error raised when executing ' + FileName + '. Message: ' + E.Message);
end;
...
end;
For testing I used this simple script:
INSERT INTO TESTTABLE
VALUES ('John', 24);
INSERT INTO TESTTABLE
VALUES ('Ed', '32');
where TESTTABLE is just a simple table containing two columns: Name NVARCHAR(50) and Age INT.
When you replace, for example, 24 by 'twentyfour' in the first INSERT statement and run the script with the Delphi code, Delphi/ADO will raise an exception. But when you replace, for example, 32 by 'thirtytwo' in the second INSERT statement, there will be no exception.
I tried to solve this by putting the script in a stored procedure "dbo.ErrorHandling" and sending
EXEC dbo.ErrorHandling
to ADO, but it did not help.
CREATE PROCEDURE dbo.ErrorHandling
AS
BEGIN
INSERT INTO TESTTABLE
VALUES ('John', 24);
INSERT INTO TESTTABLE
VALUES ('Ed', '32');
END
I can solve the problem by using TRY and CATCH in the script, and letting it log the errors to a LOGGING table. Delphi can check this table for new errors after each script execution.
However, is it possible to catch all SQL server errors in Delphi, or do I have to execute INSERTS, UPDATES,... one by one?
I use Delphi XE6 and SQLServer 2008 R2
I would t take a look at the Errors Collection of the AdoConnection.
TAdoConnection.Errors
I've never found any way of reliably processing several queries at once and sensibly detecting problems. I believe the correct solution is to execute the statements one at a time.
This is code from one of my programs doing exactly what J__ describes in his comment. It processes an SQL Server style script with a GO after every statement. You could replace the "GO" detector with some other indication of the end of a statement. It batches the whole thing up as a single all-or-nothing transaction. The last finally has some code to save the last query into an on screen memo so you can see what failed if there was an exception.
procedure TDupFrame.LoadButtonClick(Sender: TObject);
var
Query: TADOQuery;
Reader: TStreamReader;
Line: string;
begin
Query := TADOQuery.Create(nil);
try
Query.Connection := ConfModule.ADOConnection;
Query.Connection.BeginTrans;
if ScriptOpenDialog.Execute(Self.Handle) then
begin
Reader := TStreamReader.Create(ScriptOpenDialog.FileName);
try
Query.SQL.BeginUpdate;
while not Reader.EndOfStream do
begin
Line := Reader.ReadLine;
if not SameText(Line, 'GO') then
begin
Query.SQL.Add(Line);
end
else
begin
Query.SQL.EndUpdate;
Query.ExecSQL;
Query.SQL.Clear;
Query.SQL.BeginUpdate;
end;
end;
Query.SQL.EndUpdate;
if Query.SQL.Count > 0 then Query.ExecSQL;
finally
Reader.Free;
end;
Query.Connection.CommitTrans;
end;
finally
SQLMemo.Lines.Assign(Query.SQL);
// rollback if we have missed the commit (ie an exception occurred)
if Query.Connection.InTransaction then
Query.Connection.RollbackTrans;
Query.Free;
end;
end;
You need to configure the TADOQuery or TADOCommand with the ExecuteNoRecords Option:
Query := TADOQuery.Create(nil);
Query.ExecuteOptions := [eoExecuteNoRecords];
It's the same in .NET. If you use ExecuteReader() or ExecuteScalar() on a SqlCommand, no exception is thrown if the first of a sequence of statements within a single command succeeds - no matter of how many subsequent statements fail.
If you call ExecuteNonQuery() however, a proper exception is being thrown in any case.
I have one Oracle XXg database running on a remote client and it's not possible to reproduce the environment for tests. Right now, there is one stored procedure that's returning an unexpected value (it's just an integer that should be zero but is returning some other number) and I want to discover which value is that.
I can edit the functions and procedures, but I can't call the function without using a big java application that I cannot debug. Is there any way to log the return value to a text file or some log on the database so I can check it after execution?
If there's not such functionality, what would be the less troublesome workaround?
How about creating a table to log the values?
CREATE TABLE mylog (t TIMESTAMP DEFAULT SYSTIMESTAMP, retval NUMBER);
CREATE OR REPLACE PROCEDURE yourprocedure
AS
myval NUMBER;
PROCEDURE logval (myval NUMBER) AS PRAGMA autonomous_transaction;
BEGIN
INSERT INTO mylog(retval) VALUES (myval);
COMMIT;
END logval;
BEGIN
-- your normal code
myval := 1;
-- log the result
logval(myval);
END yourprocedure;
/
Then you can simply select the values from the table mylog...
In Sql Server, often times when I'm testing the body of a stored procedure, I copy the body into SSMS, DECLARE the variables at the top of the page, set them to some sample values, and execute the body as-is.
For Example, if my proc is
CREATE PROC MySampleProc
#Name VARCHAR(20)
AS
SELECT #Name
Then my test sql would be
DECLARE #Name VARCHAR(20)
SET #Name = 'Tom'
SELECT #Name
What is the Oracle PL/SQL equivalent to this?
This is the closest that I've come up with, but I'm getting "PLS-00428: an INTO clause is expected in this SELECT statement"
DECLARE
myname varchar2(20);
BEGIN
myname := 'Tom';
select myname from DUAL;
END;
This is a better example of what I'm really trying to do:
DECLARE
myname varchar2(20);
BEGIN
myname := 'Tom';
SELECT *
FROM Customers
WHERE Name = myname;
END;
But again, it wants an 'INTO' when really I just want the records printed on the screen, not stored in another table....
RESOLVED:
Thanks to #Allan, I've got it working well enough. Oracle SQL Developer apparently remembers the parameter values you supply it with. PL/SQL Developer, however, wants nothing to do with this....
If you "Run As Script", it will abide by your defaults, but it will only return results as ASCI text, not in a grid/spreadsheet
Revised Answer
If you're not calling this code from another program, an option is to skip PL/SQL and do it strictly in SQL using bind variables:
var myname varchar2(20);
exec :myname := 'Tom';
SELECT *
FROM Customers
WHERE Name = :myname;
In many tools (such as Toad and SQL Developer), omitting the var and exec statements will cause the program to prompt you for the value.
Original Answer
A big difference between T-SQL and PL/SQL is that Oracle doesn't let you implicitly return the result of a query. The result always has to be explicitly returned in some fashion. The simplest way is to use DBMS_OUTPUT (roughly equivalent to print) to output the variable:
DECLARE
myname varchar2(20);
BEGIN
myname := 'Tom';
dbms_output.print_line(myname);
END;
This isn't terribly helpful if you're trying to return a result set, however. In that case, you'll either want to return a collection or a refcursor. However, using either of those solutions would require wrapping your code in a function or procedure and running the function/procedure from something that's capable of consuming the results. A function that worked in this way might look something like this:
CREATE FUNCTION my_function (myname in varchar2)
my_refcursor out sys_refcursor
BEGIN
open my_refcursor for
SELECT *
FROM Customers
WHERE Name = myname;
return my_refcursor;
END my_function;
In Oracle PL/SQL, if you are running a query that may return multiple rows, you need a cursor to iterate over the results. The simplest way is with a for loop, e.g.:
declare
myname varchar2(20) := 'tom';
begin
for result_cursor in (select * from mytable where first_name = myname) loop
dbms_output.put_line(result_cursor.first_name);
dbms_output.put_line(result_cursor.other_field);
end loop;
end;
If you have a query that returns exactly one row, then you can use the select...into... syntax, e.g.:
declare
myname varchar2(20);
begin
select first_name into myname
from mytable
where person_id = 123;
end;
Variables are not defined, but declared.
This is possible duplicate of declare variables in a pl/sql block
But you can look here :
http://docs.oracle.com/cd/B19306_01/appdev.102/b14261/fundamentals.htm#i27306
http://docs.oracle.com/cd/B19306_01/appdev.102/b14261/overview.htm
UPDATE:
Refer here : How to return a resultset / cursor from a Oracle PL/SQL anonymous block that executes Dynamic SQL?