Adding element with index to an array in PostgreSQL - arrays

Can I specify an index (using some counter) to an element when adding elements to an array in PostgreSQL?
For example, I have this code from Oracle PL/SQL and I want to write it in PostgreSQL:
invoice_list.EXTEND;
invoice_list(counter):= cfp_cur.invoice_no; --adding some invoices from a loop to a text array
amount_list.EXTEND;
amount_list(counter) := inv_amount; --adding some amounts to a number array
counter := counter + 1; --increasing counter

PostgreSQL does not have an EXTEND method like Oracle does. PostgreSQL, however, can extend 1-dimensional arrays automatically by assigning array elements beyond the end of the current array length.
In your example, this becomes very simply:
CREATE FUNCTION some_function () RETURNS something AS $$
DECLARE
invoice_list text[];
amount_list float8[];
BEGIN
-- Do something
...
FOR counter IN 1 ... 10 LOOP
-- get current values for cfp_cur.invoice_no and inv_amount
invoice_list[counter] := cfp_cur.invoice_no;
amount_list[counter] := inv_amount;
END LOOP;
-- Do something with the arrays
...
RETURN;
END;

Related

Storing up to the most recent N elements in a Postgres jsonb field

I have a Postgres table with a JSONB column in which I want to store an array. The array starts off having 0 elements but will grow to hold up to N elements.
With every update I need to append one element to this field until the number of elements has grown from 0 to N. After that every time a new element is added the oldest one should be dropped such that the array only holds the most recent N elements.
So if the table is myTable, the JSONB array column is myArray, the incoming element is myElement and N is 10, I would have...
UPDATE myTable SET myArray=???
Can anyone provide the query that would append elements in this way?
Thanks in advance!
You could write a function that implements the check on the array length:
create or replace function append_element(p_array jsonb, p_new_element anyelement, p_max_elements int)
returns jsonb
as
$$
begin
p_array := p_array || to_jsonb(p_new_element);
while jsonb_array_length(p_array) > p_max_elements loop
p_array := p_array - 0;
end loop;
return p_array;
end;
$$
language plpgsql
immutable;
Which can be used in an UPDATE statement:
update the_table
set the_column = append_element(the_column, 'a new one', 10)
where ....;
This assumes that the_column stores the array directly, e.g. ["one", "two"] - not in some sub-structure like e.g. {"elements": ["one", "two"]}
The function could be improved by checking if the passed jsonb value is indeed an array and throwing an error otherwise.

Array loop starting after the first index in Ada?

In Ada, how do you loop across any array by any index, starting after the first index? By "any array", we mean empty arrays and arrays whose index is not an integer, too.
EDIT: If an initial conditional is required to handle corner cases - for example: empty arrays - that is fine.
EDIT: Specified "any index" instead of just the second.
Remember that the Ada 'for' loop does not include an increment operator. Instead it iterates through a range of values. The range of values may be the entire set of array indices or it may be a contiguous subset of that range.
The easiest way to accomplish this is to declare an unconstrained array type and then pass the slice of the array you want to process.
procedure main is
type Days is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
type Day_Counts is array(Days range <>) of Integer;
function Total(Item : in Day_Counts) return Integer is
Sum : Integer := 0;
begin
for Day in Item'Range loop
Sum := Sum + Item(Day);
end loop;
return Sum;
end Total;
Weekly_Counts : DayCounts := (1,2,3,4,5,6,7);
Weekly_Sum : Integer;
Weekend_Sum : Integer;
begin
Weekly_Sum := Total(Weekly_Counts);
Weekend_Sum := Total(Weekly_Counts(Sat..Sun));
end main;
The value placed in Weekly_Sum will be the sum of all 7 array elements. The value placed in Weekend_Sum will be the sum of only the Sat and Sun values.
In Ada you can specify which indexes to iterate over:
-- Declarations
Start : Index_Type;
Finish : Index_Type;
-- Usage
Start := -- Pick your start
Finish := -- Pick your end
for Index in Start .. Finish loop
-- do your stuff
end loop;
-- Example
with Ada.Text_IO; use Ada.Text_IO;
procedure Test is
type Index_Type is (Red, Blue, Green);
type Array_Type is array(Index_Type range <>) of Integer;
My_Array : Array_Type(Index_Type'Range) := (1,2,3);
Start, Finish : Index_Type;
begin
Start := Blue;
Finish := Green;
for Index in Start .. Finish loop
Put_Line(My_Array(Index)'Image);
end loop;
Put_Line("Hello World");
end Test;
where Start and End can be any index type you want. Or you can just iterate over all of them if you want and let the compiler determine what the first and last are.
This works for any type that can be an index of an array (Enumerations, Integers, etc.).
For any index type you can do things like:
Index_Type'First
Index_Type'Last
Index_Type'Succ(value)
Index_Type'Pred(value)
My_Array'Length
My_Array'Range
among many others. These should allow you to do what index math you need (again independent of the index type). See some examples below
for Index in My_Array'Range loop
if Index /= My_Array'First then
-- do stuff here
end if;
end loop;
if My_Array'First /= Index_Type'Last then
for Index in Index_Type'Succ(My_Array'First) .. My_Array'Last loop
-- Do your stuff
end loop;
end if;

Array 1 contained in array 2 and elements in the same order

Does PostgreSQL have some way for me to find if an array is contained within another array, but with the same ordering?
For example, I want to know if array1 is within array2 with matching elements in the same order.
array1[1, 3, 6, 8]
array2[3, 8, 2, 9, 10, 1, 6]
Obviously not the case in the example, but is there a built-in method for this in PostgreSQL or should I create my own function?
Version of PostgreSQL is 9.6. The actual numbers the query will run on are bigints.
General case
All elements of the second array are in the first, too. In the same order, but gaps are allowed.
I suggest this polymorphic PL/pgSQL function:
CREATE OR REPLACE FUNCTION array_contains_array_in_order(arr1 ANYARRAY
, arr2 ANYARRAY
, elem ANYELEMENT = NULL)
RETURNS bool AS
$func$
DECLARE
pos int := 1;
BEGIN
FOREACH elem in ARRAY arr2
LOOP
pos := pos + array_position(arr1[pos:], elem); -- see below
IF pos IS NULL THEN
RETURN FALSE;
END IF;
END LOOP;
RETURN true; -- all elements found in order
END
$func$ LANGUAGE plpgsql IMMUTABLE COST 3000;
As #a_horse commented, we can omit the upper bound in array subscripts to mean "unbounded" (arr1[pos:]). In older versions before 9.6 substitute with arr1[pos:2147483647] - 2147483647 = 2^31 - 1 being the theoretical max array index, the greatest signed int4 number.
This works for ...
any 1-dimensional array type, not just integer[].
arrays with NULL values, thanks to array_position() which works for NULL, too.
arrays with duplicate elements.
only for default array subscripts starting with 1. You can easily cover non-standard subscripts if needed:
Normalize array subscripts for 1-dimensional array so they start with 1
About the ANYELEMENT trick:
How do I get the type of an array's elements?
Performance
I ran a quick performance test comparing this function to the one #a_horse supplied. This one was around 5x faster.
If you use this a filter for a big table I strongly suggest you combine it with a (logically redundant) sargable filter like:
SELECT *
FROM tbl
WHERE arr #> '{2,160,134,58,149,111}'::int[]
AND array_contains_array_in_order(arr, '{2,160,134,58,149,111}')
This will use a GIN index on the array column like:
CREATE INDEX ON tbl USING gin (arr);
And only filter remaining (typically very few!) arrays that share all elements. Typically much faster.
Caveats with intarray module
Note: applies to integer[] exclusively, not smallint[] or bigint[] or any other array type!
Careful, if you have installed the intarray extension, which provides its own variant of the #> operator for int[]. Either you create an (additional) GIN index with its special operator class (which is a bit faster where applicable):
CREATE INDEX ON intarr USING gin (arr gin__int_ops);
Or, while you only have a GIN index with the default operator class, you must explicitly denote the standard operator to cooperate with the index:
WHERE arr OPERATOR(pg_catalog.#>) '{2,160,134,58,149,111}'::int[]
Details:
GIN index on smallint[] column not used or error "operator is not unique"
Simple case
As commented, your case is simpler:
The complete second array is included in the first (same order, no gaps!).
CREATE OR REPLACE FUNCTION array_contains_array_exactly(arr1 ANYARRAY, arr2 ANYARRAY)
RETURNS bool AS
$func$
DECLARE
len int := array_length(arr2, 1) - 1; -- length of arr2 - 1 to fix off-by-1
pos int; -- for current search postition in arr1
BEGIN
/* -- OPTIONAL, if invalid input possible
CASE array_length(arr1, 1) > len -- array_length(arr2, 1) - 1
WHEN TRUE THEN -- valid arrays
-- do nothing, proceed
WHEN FALSE THEN -- arr1 shorter than arr2
RETURN FALSE; -- or raise exception?
ELSE -- at least one array empty or NULL
RETURN NULL;
END CASE;
*/
pos := array_position(arr1, arr2[1]); -- pos of arr2's 1st elem in arr1
WHILE pos IS NOT NULL
LOOP
IF arr1[pos:pos+len] = arr2 THEN -- array slice matches arr2 *exactly*
RETURN TRUE; -- arr2 is part of arr1
END IF;
pos := pos + array_position(arr1[(pos+1):], arr2[1]);
END LOOP;
RETURN FALSE;
END
$func$ LANGUAGE plpgsql IMMUTABLE COST 1000;
Considerably faster than the above for longer arrays. All other considerations still apply.

Populate multidimensional array

I'm trying populate a multidimensional array on PostgreSQL, but it not work. Below my code:
CREATE OR REPLACE FUNCTION teste()
RETURNS void AS
$BODY$
DECLARE
tarifas NUMERIC[7][24];
a INTEGER;
b INTEGER;
BEGIN
FOR a IN 0..6 LOOP
RAISE NOTICE 'TESTE TESTE %', a;
FOR b IN 0..23 LOOP
RAISE NOTICE 'tarifas[%][%] = 0;', a, b;
tarifas[a][b] = 0;
END LOOP;
END LOOP;
END
$BODY$
LANGUAGE plpgsql VOLATILE;
Postgres has a dedicated function for that purpose exactly: array_fill():
returns an array initialized with supplied value and dimensions,
optionally with lower bounds other than 1
Use it:
CREATE OR REPLACE FUNCTION teste()
RETURNS void AS
$func$
DECLARE
tarifas numeric[7][24] := array_fill(0, ARRAY[7,24]);
a int;
b int;
BEGIN
-- do something
END
$func$ LANGUAGE plpgsql;
Notes
Array dimensions in numeric[7][24] are just documentation. The manual:
The current implementation does not enforce the declared number of
dimensions either. Arrays of a particular element type are all
considered to be of the same type, regardless of size or number of
dimensions. So, declaring the array size or number of dimensions in
CREATE TABLE is simply documentation; it does not affect run-time behavior.
About the assignment operator in plpgsql: := or =:
The forgotten assignment operator "=" and the commonplace ":="
It's generally not possible to write to an array element directly. You can concatenate or append / prepend elements. Or assign an array as a whole. Details in the manual. But a statement like this is not possible:
tarifas[%][%] = 0
Default lower bound of an array is 1, not 0. But you can define arbitrary array dimension. Example:
SELECT '[2:3][2:4]={{7,7,7},{7,7,7}}'::int[]

pl/sql remove element from array

I need to remove element from array. I have tried to use array.delete(n) function, but it deletes all elements from identifier n. How to just delete exact element n ?
For example if array is 1 2 3 4 5, and n = 3, after delete it should look like following 1 2 4 5.
My code so far :
DECLARE
/* declare type array */
TYPE number_index_by_number IS TABLE OF number INDEX BY binary_integer;
v_n NUMBER := &sv_n;
v_m NUMBER := &sv_m;
v_min Number;
v_tmp Number;
v_array number_index_by_number;
v_sorted_array number_index_by_number;
begin
for i in 1..v_n
loop
v_array(i) := dbms_random.value(1,1000);
end loop;
for j in v_array.first..v_array.last
loop
DBMS_OUTPUT.put_line('v_array('||j||') :'||v_array(j));
end loop;
<<i_loop>> for i in 1..v_m
loop
/*set first array value to variable min*/
v_min := v_array(1);
v_tmp := 1;
<<j_loop>> for j in v_array.first..v_array.last
loop
DBMS_OUTPUT.put_line('v_array('||j||') :'||v_array(j));
if (v_min > v_array(j)) THEN
begin
v_min := v_array(j);
v_tmp := j;
DBMS_OUTPUT.put_line(j);
end;
end if;
end loop;
/*problem is in at this line*/
v_array.delete(v_tmp);
v_sorted_array(i) := v_min;
end loop;
for i in v_sorted_array.first..v_sorted_array.last
loop
DBMS_OUTPUT.put_line('v_sorted_array('||i||') :'||v_sorted_array(i));
end loop;
end;
I cannot reproduce any of the behaviour you describe. I could not get the delete collection method to do anything other than what it is documented to do.
However, there are a few errors in your code that could do with being tidied up.
Firstly, I should point out if you delete an element with key 3 from a PL/SQL associative array, there is then nothing with key 3 in the array. The remaining values don't 'shuffle' down to fill the gap. If there was an element with key 4 before the delete, the same element will still have key 4 afterwards. As a result, if you delete element j from a PL/SQL associative array v_array and then attempt to get v_array(j), you will get a 'no data found' error. You should check to see whether the element exists, using v_array.exists(j), before attempting to get a nonexistent element.
Secondly, the element with index 1 may get deleted before the last iteration of the outer loop. If this happens, v_array(1) will fail with a 'no data found' error. It would be better to assign NULL to v_min and v_tmp at the start of the loop, and assign to them during the loop if v_min is NULL or greater than v_array(j).
Finally, it seems your code returns the v_m smallest numbers from v_n. It would be worth verifying that v_m is less than or equal to v_n, as otherwise this doesn't make sense.
I'm affraid you cannot use a built-in method like this.
Instead of you shoud create a temp array to collect the elements prior to and afterwards the selected one from the original array, and return the temp array.

Resources