Porting Oracle associative arrays to Postgres and wrapping the data access using npgsql - postgresql

It is great to get prompt replies on the npgsql queries. Thanks to its owner! I am trying to port a sproc that took in array valued parameters to postgres with similar abstraction/semantics. I am wondering how one would shape the pgsql sproc and use npgsql api to call that. I have another layer of application abstraction on top of our dal abstraction. We use data adapters to go to sql server, assoc arrays to oracle and trying to figure out what we could map this to for postgres using npgsql. We have some room in shaping the sproc but still keep the number of input params the same. we could certainly build this sproc much different but we still need it behind the same app api which supplies some set of typed arrays as shown below
public static void Flush2OraWithAssocArrayInsnetworkdatabatch(string dbKey ,int?[] ENDPOINTID,DateTime?[] INSERTEDDATETIME,int?[] RECORDTYPEID,long?[] RECORDVALUE,int?[] PACKETSIZE)
{
Database db = Helper.GetDatabase(dbKey);
using (DbConnection con = db.CreateConnection()){
con.Open();
using (DbCommand cmd = con.CreateCommand()){
cmd.CommandText = "Insnetworkdatabatch";
Helper.InitializeCommand(cmd, 300, "Insnetworkdatabatch");
BuildInsnetworkdatabatchOracleAssocArrayCommandParameters(cmd ,ENDPOINTID,INSERTEDDATETIME,RECORDTYPEID,RECORDVALUE,PACKETSIZE);
try {
Helper.ExecuteNonQuery(cmd, cmd.CommandText);
con.Close();
} catch (DALException ) {
throw;
}
}
}
}
I have a oracle sproc written as follows
create or replace PROCEDURE InsNetworkDataBatch2
(
-- Add the parameters for the stored procedure here
v_endPointID IN arrays.t_number ,
v_insertedDateTime IN arrays.t_date ,
v_recordTypeID IN arrays.t_number ,
v_recordValue IN arrays.t_number ,
v_packetSize IN arrays.t_number )
AS
BEGIN
DECLARE
BEGIN
FORALL i IN v_endpointID.FIRST..v_endpointID.LAST SAVE EXCEPTIONS
INSERT
INTO STGNETWORKSTATS
(
INSERTEDDATE,
ENDPOINTID,
RECORDTYPEID,
RECORDVALUE,
PACKETSIZE
)
VALUES
(
v_insertedDateTime(i),
v_endPointID(i),
v_recordTypeID(i),
v_recordValue(i),
v_packetSize(i)
);
END;
END;
-- END PL/SQL BLOCK (do not remove this line) ----------------------------------
Here is the assoc array package in oracle
create or replace PACKAGE Arrays AS
type t_number is table of number index by binary_integer;
type t_date is table of date index by binary_integer;
END Arrays;
Here is how we build the oracle parm and wondering what its equivalency if at all possible in postgres and trying to see how npgsql will support it
public override void CreateAssociativeArrayParameter(DbCommand cmd, string parameterName, object parameterValue, string dbType, ParameterDirection direction)
{
OracleDbType oracleDbType = dbSpecificTypesMap[dbType];
OracleParameter param = new OracleParameter(parameterName, oracleDbType, direction);
param.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
param.Value = parameterValue;
cmd.Parameters.Add(param);
}

I don't know anything about Oracle arrays or associative arrays. However, PostgreSQL has a rich support for complex types. PostgreSQL arrays are a good way to store an array of values in a column, and PostgreSQL even provides indexing and database-side functions to work with arrays.
If you're looking for a dictionary type (associative array?), take a look at hstore or json.
EDITED: If your associative array has a fixed schema (i.e. the fields don't change), you can also consider PostgreSQL composite.

Here is an attempt with Postgres stored procedure. This is now working. I got around some casting issues thrown from inside the npgsql which was a result of my .net type not being compatible with the sproc parameter data type in postgres.
Here is how i am trying to add the param value
create or replace FUNCTION InsNetworkDataBatch
(
-- Add the parameters for the stored procedure here
v_endPointID IN int[] ,
v_insertedDateTime IN timestamp[] ,
v_recordTypeID IN int[] ,
v_recordValue IN bigint[] ,
v_packetSize IN int[] ) RETURNS void
LANGUAGE 'plpgsql'
AS $$
BEGIN
DECLARE
BEGIN
FOR i IN array_lower(v_endPointID, 1) .. array_upper(v_endPointID, 1)
loop
INSERT INTO STGNETWORKSTATS
(
INSERTEDDATE,
ENDPOINTID,
RECORDTYPEID,
RECORDVALUE,
PACKETSIZE
)
VALUES
(
v_insertedDateTime[i],
v_endPointID[i],
v_recordTypeID[i],
v_recordValue[i],
v_packetSize[i]
);
end loop;
END;
END;
$$
Here is how i am trying to bind the app to the command params
public override void CreateAssociativeArrayParameter(DbCommand cmd, string parameterName, object parameterValue, string dbType, ParameterDirection direction)
{
NpgsqlDbType npgsqlDbType;
if (dbSpecificTypesMap.ContainsKey(dbType))
{
npgsqlDbType = dbSpecificTypesMap[dbType];
}
else
{
throw new ApplicationException($"The db type {dbType} could not be parsed into the target NpgsqlDbType. Please check the underlying type of the parameter");
}
NpgsqlParameter param = new NpgsqlParameter(parameterName.ToLower(), NpgsqlDbType.Array | npgsqlDbType);
param.Value = parameterValue;
cmd.Parameters.Add(param);
}

Related

Supabase calls outdated function even when it is deleted

I've created a new function in Supabase as follows:
drop function if exists select_whitelist_airdrop_address;
create
or replace function select_whitelist_airdrop_address(airdrop_address text)
returns table(owner text, hash text)
as $$
select a.owner, b.hash
from thirdparty_token_holders a
left join thirdparty_token_airdrops b
on a.id = b.holder_id
where (b.status = 1 AND b.hash != 'NOT PROCESSED' AND a.owner=airdrop_address)
limit 1
$$ language sql;
And deleted an older one called select_whitelist_airdrops. Upon deleting it, I've started getting the following error:
info: getAirdropWhitelistStatus -> error: {"error":{"message":"Could not find the public.select_whitelist_airdrops(hash) function or the public.select_whitelist_airdrops function with
a single unnamed json or jsonb parameter in the schema cache","hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache."},"da
ta":null,"count":null,"status":404,"statusText":"Not Found","body":null}
Why is Supabase referring to this function when:
I no longer need it
I am using another function in my code, see below:
public async getAirdropWhitelistStatus(address: string) {
return this.supabase.rpc('select_whitelist_airdrop_address', {
airdrop_address: address
})
}
Why does Supabase keep referring back to the old proc when run?

Spring batchUpdate with IN clause

Given a simple table
create table car (
make varchar
model varchar
)
And the following DAO code
NamedParameterJdbcTemplate template;
String SQL = "delete from car where make = :make and model in (:model)";
void batchDelete(final Map<String, Collection<String>> map) {
SqlParameterSource[] params = map.entrySet().stream()
.map(entry -> toParams(entry.getKey(), entry.getValue()))
.toArray(SqlParameterSource[]::new);
template.batchUpdate(SQL, params);
}
void delete(final Map<String, Collection<String>> map) {
map.forEach((make, models) -> {
SqlParameterSource params = toParams(make, models);
template.update(SQL, params);
});
}
SqlParameterSource toParams(final String make, final Collection<String> models) {
return new MapSqlParameterSource("make", make)
.addValue("model", new ArrayList<>(models));
}
The batch delete function fails when the maps has 2 keys with different number of values for the IN clause in a batch. Assume Map.of creates and ordered Map.
// runs fine - 2 values for each key
batchDelete(Map.of("VW", Arrays.asList("Polo", "Golf"), "Toyota", Arrays.asList("Yaris", "Camry")));
// fails - first key has 1 value, second key has 2 values
batchDelete(Map.of("Toyota", Arrays.asList("Yaris"), "VW", Arrays.asList("Polo", "Golf")));
// runs fine - key with bigger list comes first
batchDelete(Map.of("VW", Arrays.asList("Polo", "Golf"), "Toyota", Arrays.asList("Yaris")));
// non batch delete runs fine either way
delete(Map.of("Toyota", Arrays.asList("Yaris"), "VW", Arrays.asList("Polo", "Golf")));
Spring documentation sort of alludes to that
https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc-in-clause
The SQL standard allows for selecting rows based on an expression that includes a variable list of values. A typical example would be select * from T_ACTOR where id in (1, 2, 3). This variable list is not directly supported for prepared statements by the JDBC standard; you cannot declare a variable number of placeholders. You need a number of variations with the desired number of placeholders prepared, or you need to generate the SQL string dynamically once you know how many placeholders are required. The named parameter support provided in the NamedParameterJdbcTemplate and JdbcTemplate takes the latter approach.
The error message is
The column index is out of range: 3, number of columns: 2.; nested exception is org.postgresql.util.PSQLException: The column index is out of range: 3, number of columns: 2.
What happens is the following line in NamedParameterJdbcTemplate # batchUpdate:
PreparedStatementCreatorFactory pscf = getPreparedStatementCreatorFactory(parsedSql, batchArgs[0]);
will create a dynamic sql out of the first batch arg length:
delete from car where make = ? and model in (?)
So the 2nd batch item which has 2 models will fail as there is only 1 placeholder.
What would be a workaround ? (other than grouping map entries by number of values)
Solution
Went back to plain old PreparedStatement
SQL - use ANY instead of IN
delete from car where make = ? and model = any (?)
DAO
Connection con;
PreparedStatement ps = con.prepareStatement("SQL");
map.forEach((make, models) -> {
int col = 0;
ps.setString(++col, make);
ps.setArray(++col, con.createArrayOf("text", models));
ps.addBatch();
});
ps.executeBatch();
I would recommend changing the SQL to look something more like this:
String SQL = "DELETE FROM car WHERE (make, model) IN (:ids)";
If you do it this way then you can use something similar to the answer I gave on this question: NamedJDBCTemplate Parameters is list of lists. Doing it this way means you can use NamedParameterJdbcTemplate.update(String sql, Map<String, ?> paramMap). Where in your paramMap the key would be "ids" and the value would be an instance of Collection<Object[]> where each entry in the collection is an array containing the value pairs you want to delete:
List<Object[]> params = new ArrayList<>();//you can make this any instance of collection you want
for (Car car : cars) {
params.add(new Object[] { car.getMake(), car.getModel() });
//this is just to provide an example of what I mean, obviously this will probably be different in your app.
}

Forced PostgreSQL type convertion using JOOQ tool

Is there a way to configure JOOQ tool to convert smallint to Boolean using 'forcedTypes' tag for PostgresSQL database, without providing org.jooq.Converter implementation?
This is how the current configuration looks like:
<forcedTypes>
<forcedType>
<name>BOOLEAN</name>
<types>smallint.*</types>
</forcedType>
<forcedTypes>
JOOQ v3.9.1 is being used.
PostgreSQL v9.6.6.
And unfortunately receives the next exception while storing information into the database:
Caused by: org.postgresql.util.PSQLException: ERROR: column "is_complete" is of type smallint but expression is of type boolean
Also tried with MySQL database and similar convertion from tinyint to Boolean works fine without any errors:
<forcedTypes>
<forcedType>
<name>BOOLEAN</name>
<types>tinyint.*</types>
</forcedType>
</forcedTypes>
No, this doesn't work as you're expecting (and it shouldn't). In jOOQ, the BOOLEAN data type is bound to JDBC as the native BOOLEAN type if the database supports it, e.g. PostgreSQL.
If the database doesn't support the type (e.g. MySQL / Oracle), then jOOQ will bind 0/1/NULL number values. But you cannot enforce this behaviour for a dialect that would otherwise support BOOLEAN types. But then again, why not just write that converter? It's really simple. Just add:
<forcedTypes>
<forcedType>
<userType>java.lang.Boolean</userType>
<converter>com.example.BooleanAsSmallintConverter</converter>
<!-- A bit risky. Are all smallints really booleans in your database? -->
<types>smallint.*</types>
</forcedType>
<forcedTypes>
And then:
class BooleanAsSmallintConverter extends AbstractConverter<Short, Boolean> {
public BooleanAsSmallintConverter() {
super(Short.class, Boolean.class);
}
#Override
public Boolean from(Short t) {
return t == null ? null : t.shortValue() != (short) 0;
}
#Override
public Short to(Boolean u) {
return u == null ? null : u ? Short.valueOf((short) 1) : Short.valueOf((short) 0);
}
}
You could do that, but I doubt that this is what you had in mind. You will have to create custom cast in PostgreSQL:
CREATE FUNCTION bool2int2(IN bool, OUT int2)
LANGUAGE SQL
AS $$
SELECT CASE WHEN $1 THEN 1::int2 WHEN $1 IS NOT NULL THEN 0::int2 END
$$;
DROP CAST IF EXISTS (bool AS int2);
CREATE CAST (bool AS int2)
WITH FUNCTION bool2int2(bool)
AS ASSIGNMENT;
Then this will work:
DROP TABLE IF EXISTS booltest;
CREATE TABLE booltest (id serial, boolval int2);
INSERT INTO booltest (boolval) VALUES(true),(false),(null::bool);
SELECT * FROM booltest;
id | boolval
----+---------
1 | 1
2 | 0
3 | (null);

Stop jOOQ stripping characters from SQL field

I am implementing a function to get an estimate of the count as described in the PostgreSQL documentation here https://wiki.postgresql.org/wiki/Count_estimate
I'm using the function:
public static Field<Integer> countEstimate(final QueryPart query) {
final String sql = String.format("count_estimate(%s)", escape(query.toString()));
return field(sql(sql), PostgresDataType.INT);
}
Which looks fine until I pass it an IN clause array field in the query. When this happens jOOQ strips the array curly braces from within my SQL. e.g. Calling it with this java code:
final UUID[] ids = new UUID[]{UUID.randomUUID()};
return db.select(countEstimate(db.select(TABLE.ID)
.from(TABLE)
.where(overlaps(ids, TABLE.FILTER_IDS))));
Results in both the variable sql and DSL.sql(sql) in the above function rendering:
count_estimate(E'select "schema"."table"."id"
from "schema"."table"
where (
((\'{"75910f3b-83e6-41ed-bf57-085c225e0131"}\') && ("schema"."table"."filter_ids"))
)')
But field(sql(sql), PostgresDataType.INT) renders this:
count_estimate(E'select "schema"."table"."id"
from "schema"."table"
where (
((\'"75910f3b-83e6-41ed-bf57-085c225e0131"\') && ("schema"."table"."filter_ids"))
)')
Is there any way to work around this and to tell jOOQ to leave my query alone?
(jOOQ 3.8.3, PG 9.5.5, PG driver 9.4-1203-jdbc4)
It turns out it only strips '{}' style arrays. Replacing the code that turns the UUID[] into sql from
DSL.val(ids)
with
DSL.array(Arrays.stream(ids)
.map(UUID::toString)
.collect(Collectors.toList())
.toArray(new String[0]))
.cast(PostgresDataType.UUID.getArrayDataType()
results in it rendering cast(array[\'75910f3b-83e6-41ed-bf57-085c225e0131\'] as uuid[]) prevents it being stripped

Stored Procedure returns incorrect scalar value of -1, instead of return value

I am trying to return a scalar value from a stored procedure. I actually want to return the ID of a newly created record, but I have simplified my problem down to a stored procedure that takes an int and attempts to return that same int. This always returns -1. Thank you very much for your help.
Web API Controller call
var idtest = dbconn.my_return_int(123);
The stored procedure:
ALTER PROCEDURE [dbo].[my_return_int]
#ID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE #return as int
SET #return = -999
RETURN #return
END
The Context generated stored procedure call
public virtual int my_return_int(Nullable<int> iD)
{
var iDParameter = iD.HasValue ?
new ObjectParameter("ID", iD) :
new ObjectParameter("ID", typeof(int));
return (IObjectContextAdapter)this).ObjectContext.ExecuteFunction("my_return_int", iDParameter);
}
When you execute ObjectContext.ExecuteFunction the result is:
from MSDN: discards any results returned from the function; and returns the number of rows affected by the execution
I.e. it doesn't return the output parameter, because it doesn't know there is one. Besides, as you have called SET NOCOUNT ON; in your stored procedure, it doesn't even return the number of affected rows, thus you get the -1.
So, you must do two changes:
change your procedure so that it doesn't return the value, but selects it, i.e. instead of RETURN #return do SELECT #return AS alias. NOTE that you need the "AS alias" part. The alias can be whatever column name you want.
change the mapping of the stored procedure, so that it expects an int32 value. If you have doubt, refer to this Q&A.
In this way you'll read the return value as a result set, instead of a return value.
to solve this entity framework issue and assuming you already have a stored procedure working and you don't want to fix the places where it works good using other ways then EF you can replace the:
RETURN(0)
with:
BEGIN
SELECT(0)
RETURN(0)
END
that is if its a part of an IF sentence,
theמ go to:
EF model (.edmx) -> Model Browser -> Function Imports -> (doubl click your func) ->Scalars -> Int32
now your return value is:
ObjectResult<Nullable<int>>
you can receive it by writing:
int? response = 0;
response = myEntity.myStoredProcedure(var1, var2).FirstOrDefault();
or in pics:
1.
2.
3.
doing all this makes the EF model (.edmx) also supporting the 'Update Model from Database...' and you can update it without worry to changes you just made.
Thank you, DavidG for the article. It got me started down the right path. So what I did to solve this was change my Stored Procedure to return an ObjectResult<int?> instead of an int. Then I did a SingleOrDefault() on the results of the Stored Procedure call, which yielded my int return value. Like this:
Stored Proc:
-- RETURN #return does not work. Can't return a scalar value
SELECT #return -- This returns a result set with a single object that contains an int.
Then, my generated code looks like above but instead of returning an int it returns an ObjectResult<int?>
and I read the results like this:
var id = myDbContext.My_Return_Int(123).SingleOrDefault();
I've just implemented another option in my project. I feed in another output parameter for #return.