NPOCO PL/SQL HANDELING OUT PARAMETERS - oracle

I have stored procedure in Oracle
CREATE OR REPLACE PROCEDURE procTest (p_param1 varchar2, p_param2 out varchar2)
AS
BEGIN
...
END;
How can I handle out parameters?
_db.Execute("EXEC procTest ('dsds')");

reed about Stored Procedures And Functions Support on http://pocoproject.org/docs-1.5.0/00200-DataUserManual.html
Stored Procedures And Functions Support
Most of the modern database systems support stored procedures and/or functions. Does Poco::Data provide any support there? You bet. While the specifics on what exactly is possible (e.g. the data types passed in and out, automatic or manual data binding, binding direction, etc.) is ultimately database dependent, POCO Data does it's best to provide reasonable access to such functionality through in, out and io binding functions. As their names imply, these functions are performing parameters binding tho pass in or receive from the stored procedures, or both. The code is worth thousand words, so here's an Oracle ODBC example:
session << "CREATE OR REPLACE "
"FUNCTION storedFunction(param1 IN OUT NUMBER, param2 IN OUT NUMBER) RETURN NUMBER IS "
" temp NUMBER := param1; "
" BEGIN param1 := param2; param2 := temp; RETURN(param1+param2); "
" END storedFunction;" , now;
int i = 1, j = 2, result = 0;
session << "{? = call storedFunction(?, ?)}", out(result), io(i), io(j), now; // i = 2, j = 1, result = 3
Stored procedures are allowed to return data sets (a.k.a. cursors):
typedef Tuple<std::string, std::string, std::string, int> Person;
std::vector<Person> people;
int age = 13;
session << "CREATE OR REPLACE "
"FUNCTION storedCursorFunction(ageLimit IN NUMBER) RETURN SYS_REFCURSOR IS "
" ret SYS_REFCURSOR; "
"BEGIN "
" OPEN ret FOR "
" SELECT * FROM Person WHERE Age < ageLimit; "
" RETURN ret; "
"END storedCursorFunction;" , now;
session << "{call storedCursorFunction(?)}", in(age), into(people), now;
The code shown above works with Oracle databases.

Related

How do I iterate the Enum captions in Business Central 365

Enums are/will be the Option replacements for Business Central 365. Recently I had an occasion to use a few and get my feet wet, so to speak. As seems to be the case far too often, about 80% of the functionality you need is readily available, but the remaining 20% takes more work than it should.
In the case of Enums, you get a Text list of Names and an Integer list of associated Ordinal values, but you do NOT get a list of Captions. In the partial example Enum FourStates below, Default, OH and TX are the Names, 0, 1 and 2 are the Ordinals and space, Ohio and Texas are the captions. Note that Ordinals are the defined numeric values, NOT the indexes. Perfectly valid Ordinals in the example below could be 1, 5 and 7.
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
If you define a Table or Page field as an Enum, then the captions are displayed in the dropdowns. To get the Caption you can use Format(EnumType::Name) but we needed to iterate all of the Captions for a given Enum.
After digging around in some blogs and the documentation, here is a summary of what I found.
First, there is a major limitation because Captions can ONLY be processed within the context of an Enum type and, at least as of now, the Business Central 365 SaaS solution has no support for generic Enums (e.g. EnumRef like RecordRef and FieldRef). This means a Text list of Captions must be created on an Enum by Enum basis.
However, once the Captions list has been created, then using the combination of Name, Ordinal and Caption Lists of [Text], you can then write general purpose code that will work for any Enum.
A Gotcha, when you use the TEnum code snippet in VS Code, it defaults the first element to 0. We have learned to either make that the Default as the partial code above does, or change it to 1. The reason for this is because if there is any chance that you will ever use an Enum with the StrMenu command, StrMenu defaults 0 as the Cancel return value. In that case, you could never select the 0 Ordinal Enum because ti would be treated as Canceled.
To create an instance of an Enum to use within your code, create a Var variable similar to MyChoices: Enum "Defined Choices";. If you also define a MyChoice: Enum: "Defined Choices", you can compare them with something like if MyChoice = MyChoices::FirstChoice then, etc.
Below is some sample code with a few different Enum styles along with a method that allows you to create a List of [Text] for the Captions. Again, that has to be coded for each specific Enum.
a. Within VS Code, use AL Go to create a new HelloWorld app
b. Change the app.json file Name and Publisher, I named mine EnumHelper
NOTE: We always define a Publisher because you cannot filter for Default Publisher within SaaS
c. Replace all of the code inside the HelloWorld.al file with the code below
NOTE: To simplify things, everything below is all in the same file
d. The code is a PageExtension for the Chart of Accounts Page and runs off the OnOpenPage trigger
This allows the code to be easily called without requiring a bunch of setup code.
Here is the code that allows you to create a Captions list. The variable myOrdinal is an Integer and Flagged is a defined Enum. Note the Enum::EnumName, similar to Page::PageName or Database::TableName.
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
All of the code (sorry, it didn't format exactly correctly)
enum 50200 FourStates
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; OH) { Caption = 'Ohio'; }
value(2; TX) { Caption = 'Texas'; }
value(3; NC) { Caption = 'North Carolina'; }
value(4; IA) { Caption = 'Iowa'; }
value(5; MO) { Caption = 'Missouri'; }
}
enum 50201 Flagged
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Bold) { Caption = 'Bold'; }
value(2; ITalic) { Caption = 'Italid '; }
value(4; Underline) { Caption = 'Underline'; }
value(8; BoldItalic) { Caption = 'Bold & Italic'; }
value(16; BoldUnderline) { Caption = 'Bold & Underline '; }
value(32; ItalicUnderline) { Caption = 'Italic & Underline'; }
value(64; All3Options) { Caption = 'All 3 Options'; }
}
enum 50202 Randomized
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(7; Good) { Caption = 'The Good'; }
value(5; Bad) { Caption = 'The Bad'; }
value(11; Ugly) { Caption = 'The Ugly'; }
}
enum 50203 ProcessFlowOptions
{
Extensible = true;
value(0; Default) { Caption = ' '; }
value(1; Flagged) { Caption = 'Flagged'; }
value(2; Randomized) { Caption = 'Randomized'; }
value(4; FourStates) { Caption = 'FourStates'; }
}
pageextension 50200 "Chart of Accounts EH" extends "Chart of Accounts"
{
var
// Enum instance variables.
myFlagged: Enum Flagged;
myFourStates: Enum FourStates;
myRandomized: Enum Randomized;
trigger OnOpenPage();
begin
case UserID.ToLower() of
'larry':
Message('Hello Larry, this is an extension for October testing.');
'vicki':
Message('Good morning Vicki, this is an extension for October testing.');
else
if Confirm('Hello %1 from EnumHelper.\\Click Yes to process or no to cancel.', true, UserID) then begin
ProcessEnumerations();
end;
end;
end;
local procedure ProcessEnumerations()
var
allLines: TextBuilder;
randomCaptions: List of [Text];
flaggedCaptions: List of [Text];
fourStatesCaptions: List of [Text];
begin
GetEnumCaptions(randomCaptions, flaggedCaptions, fourStatesCaptions);
IterateEnumNamesOrdinalsAndCaptions(allLines, randomCaptions, flaggedCaptions, fourStatesCaptions);
Message(allLines.ToText());
end;
local procedure GetEnumCaptions(randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
GetCaptions(randomCaptions, ProcessFlowOptions::Randomized);
GetCaptions(flaggedCaptions, ProcessFlowOptions::Flagged);
GetCaptions(fourStatesCaptions, ProcessFlowOptions::FourStates);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(allLines: TextBuilder; randomCaptions: List of [Text]; flaggedCaptions: List of [Text]; fourStatesCaptions: List of [Text])
begin
IterateEnumNamesOrdinalsAndCaptions('Flagged Enum', allLines, myFlagged.Names, myFlagged.Ordinals, flaggedCaptions);
IterateEnumNamesOrdinalsAndCaptions('Randomized Enum', allLines, myRandomized.Names, myRandomized.Ordinals, randomCaptions);
IterateEnumNamesOrdinalsAndCaptions('FourStates Enum', allLines, myFourStates.Names, myFourStates.Ordinals, fourStatesCaptions);
end;
local procedure IterateEnumNamesOrdinalsAndCaptions(title: Text; allLines: TextBuilder; enumNames: List of [Text]; enumOrdinals: List of [Integer]; enumCaptions: List of [Text])
var
i: Integer;
enumLine: TextBuilder;
enumLines: TextBuilder;
begin
allLines.AppendLine(title);
allLines.appendLine();
for i := 1 to enumNames.Count do begin
Clear(enumLine);
enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''',');
enumLine.AppendLine('EnumOrdinal: ' + Format(enumOrdinals.Get(i)) + ',');
enumLine.AppendLine('EnumCaption: ''' + enumCaptions.Get(i) + '''.');
//enumLine.AppendLine('EnumName: ''' + enumNames.Get(i) + ''', EnumOrdinal: ' + ordinal + ', EnumCaption: ''' + enumCaptions.Get(i) + '''');
enumLines.AppendLine(enumLine.ToText());
end;
allLines.AppendLine(enumLines.ToText());
end;
local procedure GetCaptions(captions: List of [Text]; processFlowOption: Enum ProcessFlowOptions)
var
myOrdinal: Integer;
myProcessFlowOptions: Enum ProcessFlowOptions;
begin
// Load captions by iterating specific Enums.
case processFlowOption of
myProcessFlowOptions::Flagged:
begin
foreach myOrdinal in Flagged.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Flagged.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::Randomized:
begin
foreach myOrdinal in Randomized.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::Randomized.FromInteger(myOrdinal)));
end;
end;
myProcessFlowOptions::FourStates:
begin
foreach myOrdinal in FourStates.Ordinals do begin
// Enum definition, NOT an enum instance.
captions.Add(Format(Enum::FourStates.FromInteger(myOrdinal)));
end;
end;
end;
end;
}
Enjoy
I have implemented a better workaround for BC14. That should work on newer versions also, but I have tested on BC14 only.
var
RecRef: RecordRef;
FRef: FieldRef;
OptionNames: List of [Text];
OptionCaptions: List of [Text];
i: Integer;
RecRef.GetTable(Rec); // Some record
FRef := RecRef.Field(20); // Some field of type Enum
OptionNames := FRef.OptionMembers().Split(',');
OptionCaptions := FRef.OptionCaption().Split(',');
for i := 1 to OptionNames.Count() do begin
Evaluate(FRef, OptionNames.Get(i));
Message('Ordinal = %1\Name = %2\Caption = %3',
format(FRef, 0, 9),
OptionNames.Get(i),
format(FRef, 0, 1)); // or OptionCaptions.Get(i)
end;

What is wrong with my Oracle RAW return param?

I have a stored procedure with the following signature and local variables:
PROCEDURE contract_boq_import(i_project_id IN RAW,
i_boq_id IN RAW,
i_master_list_version IN NUMBER,
i_force_update_if_exists IN BOOLEAN,
i_user_id IN NUMBER,
o_boq_rev_id OUT RAW) AS
v_contract_id RAW(16);
v_contract_no VARCHAR2(100);
v_series_rev_id_count NUMBER(1);
v_project_id_count NUMBER(5);
v_now DATE;
v_boq_import_rev_id RAW(16);
v_master_project_id RAW(16);
v_prj_duplicate_items VARCHAR2(1000) := '';
I set up an output parameter using one of our DAL utilities:
var revParam = new byte[16];
dataHandler.CreateParameterRaw("o_boq_rev_id", revParam).Direction = ParameterDirection.Output;
Where CreateParameterRaw is declared as:
public DbParameter CreateParameterRaw(string name, object value)
{
OracleParameter oracleParameter = new OracleParameter();
oracleParameter.ParameterName = name;
oracleParameter.OracleDbType = OracleDbType.Raw;
oracleParameter.Value = value;
this.Parameters.Add((DbParameter) oracleParameter);
return (DbParameter) oracleParameter;
}
Then when I execute the procedure with ExecuteNonQuery I get the following error:
Oracle.ManagedDataAccess.Client.OracleException
HResult=0x80004005
Message=ORA-06502: PL/SQL: numeric or value error: raw variable length too long
ORA-06512: at "ITIS_PRCDRS.PA_PRJ_IMP", line 1235
The exception is thrown on line 1235:
o_boq_rev_id := v_boq_import_rev_id;
As you can see from the procedure declaration above, v_boq_import_rev_id has type RAW(16) and o_boq_rev_id has type OUT RAW, so why should the assignment on line 1235 fail? What am I doing wrong?
PS: The proc executes fine when I call it in plain PL/SQL.
In OracleParameter the default size is 0 for the parameters that may have size values. (Official reference here.)
That is why you need to modify your method which generates the raw values. Below you can find the modified method:
public DbParameter CreateParameterRaw(string name, object value, int parameterSize)
{
OracleParameter oracleParameter = new OracleParameter();
oracleParameter.ParameterName = name;
oracleParameter.OracleDbType = OracleDbType.Raw;
oracleParameter.Value = value;
oracleParameter.Size = parameterSize; /* THIS IS THE ADDED PARAMETER */
this.Parameters.Add((DbParameter) oracleParameter);
return (DbParameter) oracleParameter;
}
And as a result you can pass the size while calling CreateParameterRaw as you did in your existing code:
var revParam = new byte[16];
/* CHECK THE 16 value in the parameters that are sent to CreateParameterRaw */
dataHandler.CreateParameterRaw("o_boq_rev_id", revParam, 16).Direction = ParameterDirection.Output;
Additional suggestion: In order to keep apples with apples, I would suggest you can take Direction parameter also into the CreateParameterRawmethod. By this way CreateParameterRawbecomes the whole responsible about generating the parameters.
Credits:
Official page: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/odpnt/ParameterCtor5.html#GUID-04BE7E69-A80A-4D28-979A-CDC2516C0F93
A blog that has similar problem: http://devsilos.blogspot.com/2013/01/ora-06502-with-out-parameter-called.html?m=1
Size usage from Microsoft: https://learn.microsoft.com/en-us/dotnet/api/system.data.oracleclient.oracleparameter.size?view=netframework-4.8
This is an interesting problem with a weird solution.
Actually while using the RAW in output parameter you MUST provide some buffer space for it when adding this parameter.
Can you please provide some buffer space for this variable and try something like the following:
byte[] RAWPlaceholder = new byte[16];
cmd.AddParameter(new OracleParameter("o_boq_rev_id",
OracleDbType.Raw,
16,
RAWPlaceholder,
ParameterDirection.Output);
Please share the result of the aforementioned exercise.
Thanks

Oracle: Error ORA-00932: inconsistent Datatypes number assumed, but got ARRAY

We are trying to select an array/varray type with embedded SQL.
Type/Table:
create type int55 is array(4) of integer;
create table rtable (a int, b int55);
Code for SQL Precompiler in C++ with Oracle 11g Client:
BOOL static_TestIt(void) {
EXEC SQL BEGIN DECLARE SECTION;
int a;
long b[4];
short i[4];
EXEC SQL END DECLARE SECTION;
EXEC SQL DECLARE cdtest_cursor CURSOR FOR select a,b from rtable;
EXEC SQL open cdtest_cursor;
EXEC SQL FETCH cdtest_cursor INTO :a, :b:i;
EXEC SQL close cdtest_cursor;
return TRUE;
}
The precompiler shows warning:
PCC-W-02344 invalid size of host array
During runtime i get error: ORA-00932
Does anybody know a solution to this problem?
You need to do a few more steps, based on the documentation on collection handling in Pro*C.
First, create a file called int55_in.typ containing the name of your collection type:
case=lower
type int55
Then run the object type translator tool:
$ORACLE_HOME/bin/ott intype=int55_in.typ outtype=int55_out.typ hfile=int55.h user=usr/pwd code=c
which will generate two files. In your .pc file add:
#include "int55.h"
and your code needs to use that declared collection type:
EXEC SQL BEGIN DECLARE SECTION;
int a;
int55 *b; /* collection type */
int c; /* single instance from collection */
OCIInd i; /* indicator */
EXEC SQL END DECLARE SECTION;
EXEC SQL ALLOCATE :b;
EXEC SQL DECLARE cdtest_cursor CURSOR FOR select a, b from rtable;
EXEC SQL open cdtest_cursor;
EXEC SQL FETCH cdtest_cursor INTO :a, :b:i;
Then you can use COLLECTION GET to get the individual elements; as it's a varray you can loop over the four entries, e.g. as a simple demo:
int j;
for (j = 0; j < 4; j++)
{
EXEC SQL COLLECTION GET :b INTO :c;
printf("%d %d %d\n", j, a, c);
}
EXEC SQL close cdtest_cursor;
When you compile you need tomake sure the location for oci.h (included from the generated int55.h) if it isn't already referred to; and you need to specify the generated type file:
$ORACLE_HOME/bin/proc include=$ORACLE_HOME/rdbms/public intyp=int55_out.typ ...
With a single row in your table:
insert into rtable values (1, int55(2, 3, 4, 5));
running that then gets:
0 1 2
1 1 3
2 1 4
3 1 5
Obviously you should pick your own file names, I've just used your type name as an example to hopefully make it clearer. And if you have to handle mutlipel types they can all go in a single _in.typ to generate a single _out.typand.h` file.

Return one value from a table using a function

I have this code:
public int GetUserIdByEmail(string email)
{
using (SqlConnection conn = new SqlConnection(ZincModelContainer.CONNECTIONSTRING))
{
using (SqlCommand cmd = conn.CreateCommand())
{
conn.Open();
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = String.Concat("SELECT [Zinc].[GetUserIdByEmail] (", email, ")"); //is this correct??? the problem lies here
return (int)cmd.ExecuteScalar();
}
}
}
I get the error here in above code. this is still not right
I have my function now as below suggested by veljasije
thanks
Modify your procedure:
CREATE PROCEDURE [Zinc].[GetUserIdByEmail]
(
#Email varchar (100)
)
AS
BEGIN
SELECT zu.UserId from Zinc.Users zu WHERE Email = #Email
END
And in you code change type of parameter from NVarChar to VarChar
Function
CREATE FUNCTION [Zinc].[GetUserIdByEmail]
(
#Email varchar(100)
)
RETURNS int
AS
BEGIN
DECLARE #UserId int;
SET #UserId = (SELECT zu.UserId from Zinc.Users zu WHERE Email = #Email)
RETURN #UserId
END
Firstly, specify the size for the #Email parameter in the sproc - without it, it will default to 1 character which will therefore not be attempting to match on the value you are expecting it to.
Always specify the size explicitly to avoid any issues (e.g. per Marc_s's comment, plus demo I blogged about here, it behaves differently bu defaulting to 30 chars when using CAST/CONVERT )
Secondly, use SqlCommand.ExecuteScalar()
e.g.
userId = (int)cmd.ExecuteScalar();

Storing .NET double value in Oracle DB

I'm using ODP.NET to access Oracle DB from C# .NET.
Please see following code:
OracleConnection con = new OracleConnection();
con.ConnectionString = "User Id=user;Password=pass;Data Source=localhost/orcl";
con.Open();
/* create table */
DbCommand command = con.CreateCommand();
command.CommandType = CommandType.Text;
try
{
command.CommandText = "DROP TABLE TEST";
command.ExecuteNonQuery();
}
catch
{
}
//command.CommandText = "CREATE TABLE TEST (VALUE BINARY_DOUBLE)";
command.CommandText = "CREATE TABLE TEST (VALUE FLOAT(126))";
command.ExecuteNonQuery();
/* now insert something */
double val = 0.8414709848078965;
command.CommandText = "INSERT INTO TEST VALUES (" + val.ToString(System.Globalization.CultureInfo.InvariantCulture) + ")";
command.ExecuteNonQuery();
/* and now read inserted value */
command.CommandText = "SELECT * FROM TEST";
DbDataReader reader = command.ExecuteReader();
reader.Read();
double res = (double) (decimal)reader[0];
Console.WriteLine("Inserted " + val + " selected " + res);
The output from this is always:
Inserted 0,841470984807897 selected 0,841470984807897
But looking at variable values under debugger
val == 0.8414709848078965
res == 0,841470984807897
Why res is rounded up?
I looked into DB and there is stored rounded-up value.
On the other hand I used Oracle SQL Developer to modify this value, and I'm able to store 0.8414709848078965 in database?
I tried types NUMBER, FLOAT(126), BINARY_DOUBLE... always the same result.
Why there is a problem using ODP.NET?
OK, I have found that it works if parameter type is OracleDbType.BinaryDouble. But it causes my code to be dependent of ODP.NET. I wanted to use ADO.NET types (DbType) to achieve my code independency.
Oracle actually has a higher precision for it's numbers than .net!
I tried this in straight Oracle and it works fine, I recommend changing to use a param
e.g.
-- CREATE TABLE TEST (VALUE NUMBER(38,38)); (initial test)
INSERT INTO TEST VALUES (0.8414709848078965);
SELECT * FROM TEST;
VALUE
----------------------
0.8414709848078965
(recommendation)
OracleParameter param = cmd.CreateParameter();
param.ParameterName = "NUMBERVALUE";
param.Direction = ParameterDirection.Input;
param.OracleDbType = OracleDbType.Decimal;
param.Value = "0.8414709848078965";
command.Parameters.Add(param);

Resources