Early binding of CURRENT_SCHEMA (Was: CREATE FUNCTION ... SEARCH {, DEFAULT | SYSTEM | SESSION })

2023-09-26 Thread dld

Hi there!

I followed the discussion about the schema resolution, and I really 
think there is need for an early bound (at function definition time) 
version of CURRENT_SCHEMA (the first member of search_path)


Avoiding hard-coding of schema names, (and avoiding polluting the actual 
users schema) is hard.


My current code generates some plpgsql functions, which need some helper 
functions to construct fcolumn lists, query fragments, etc.


These helpers should live in the same schema, IMHO


It is not impossible: I ended up with the following kludge to refer to 
functions in the same schema as the calling function.


It involves an extra layer of dynamic SQL, which self-destructs after 
use. It is not pretty, but it works.


Example of such a nested dynamic function is attached. (the helper 
functions are not included, but they live in the same "compilation unit")


Cheers,

Adriaan van Kessel
-- -
CREATE OR REPLACE FUNCTION disposable_factory ()
RETURNS text
VOLATILE
LANGUAGE plpgsql
AS
$WTF$
DECLARE
 _fmt text;
 _sql text;
BEGIN

_fmt = $FMT$
-- "Factory function"
-- Create a table-returning function for table "_fqn"
-- with the same columns.
-- But: restricted to the most recent, upto (and including)  asof_date.
-- The function name is the table name, with '_asof' appended,
-- and it is created in the same schema as the table.
-- The generated function takes one argument _datum
-- , with the same type as tbl.asof_date
-- 
CREATE OR REPLACE FUNCTION create_asof (_fqn text, asof_date text )
RETURNS text
VOLATILE
SECURITY INVOKER
LANGUAGE plpgsql
AS
$func$
DECLARE sql text;
basepair text[];
funcpair text[];
fnc_name text;
org_name text;
allkeys text[];
keys text[];
BEGIN

basepair := %1$s.split_name (_fqn);

funcpair[1] = basepair[1];
funcpair[2] = concat(basepair[2] , '_asof' );

org_name := %1$s.format_pair(basepair);
fnc_name := %1$s.format_pair(funcpair);

allkeys := %1$s.fetch_pk_array (basepair[1] , basepair[2] , asof_date );
keys := array_remove (allkeys, asof_date);

-- RAISE NOTICE 'Allkeys=%%' , all_keys[1];
-- RAISE NOTICE 'Keys=%%' , keys[1];

-- CREATE OR REPLACE FUNCTION %%1$s (IN _datum DATE DEFAULT now() ) -- name
sql := format ('
CREATE OR REPLACE FUNCTION %%1$s (IN _datum %%2$s DEFAULT now() ) -- fnc_name 
dtype
RETURNS SETOF %%3$s -- orgtable
STABLE SECURITY INVOKER ROWS 30
LANGUAGE sql
AS
$omg$
SELECT *-- all columns
FROM %%3$s src  -- org table
WHERE %%4$s -- date treshold
AND NOT EXISTS (
SELECT * 
FROM %%3$s nx   -- org table
WHERE %%5$s -- key fields
AND %%6$s   -- date treshold
AND %%7$s   -- gap
)
;
$omg$ 
;'
, fnc_name  -- 1 Function 
name
, %1$s.fetch_typename(basepair[1], basepair[2], asof_date)  
-- 2 typeof Date field argument
, org_name  -- 3 table name
, %1$s.format_reference ('src', asof_date) || ' <= $1'  -- 4 source 
Date treshold
, %1$s.format_equal_and_list ('nx', 'src', keys)-- 5 
Same Keys
, %1$s.format_reference ('nx', asof_date) || ' <= $1'   -- 6 nx Date 
treshold
, %1$s.format_reference ('nx', asof_date) || ' > ' || 
%1$s.format_reference( 'src', asof_date)  -- 7 Gap Date
);

-- RAISE NOTICE 'Pair= [%%,%%]' , basepair[1], basepair[2];
-- RAISE NOTICE 'Fnc=%%' , fnc_name;
-- RAISE NOTICE 'Sql=%%' , sql;
EXECUTE sql;
-- RETURN sql ;
RETURN fnc_name ;
END;
$func$;
$FMT$ ;

-- RAISE NOTICE '_Fmt=%' , _fmt;
_sql = format (_fmt, quote_ident(current_schema) );
-- RAISE NOTICE '_Sql=%', _sql;

EXECUTE _sql;
DROP FUNCTION disposable_factory (); -- suicide
-- return _sql;
return 'create_asof';
END;
$WTF$ ;

\echo SELECT disposable_factory();
SELECT disposable_factory();
-- \df create_asof

-- EOF


Re: Early binding of CURRENT_SCHEMA (Was: CREATE FUNCTION ... SEARCH {, DEFAULT | SYSTEM | SESSION })

2023-09-27 Thread dld

On 27-09-2023 04:03, Erik Wienhold wrote:

ccing list

On 2023-09-27 00:12 +0200, dld write:

On 26-09-2023 23:47, Erik Wienhold wrote:

On 2023-09-26 14:44 +0200, dld wrote:

I followed the discussion about the schema resolution, and I really think
there is need for an early bound (at function definition time) version of
CURRENT_SCHEMA (the first member of search_path)

The helper functions can't be created in a common schema with a fixed
name?



Yes, they could. But I try to avoid hard coding the name all over the place.



Ah, I see.  But still wondering if this is necessary.


I do not want to interfere, I do not want to pollute their schema with my
nonsense-functions..

Again, why can't create_asof() and the helper/worker functions be in a
hard coded schema?  Are those functions defined once in the database or
does each user get their own version, perhaps in a multitenancy design?
And who is calling create_asof()?


Yes they could.

my_separate_schema.create_asof() is intended to be called by the "end 
user" of the "package"


From whatever current_schema or search_path [s]he happens to be in.




I'm currently working on a database that I also designed in large parts
where trigger functions (SECURITY DEFINER) create views that give users
a restricted view of the data for ease of use.  Quite similar to that
create_asof() function but with hard coded schema names.  So I'm also
interested to learn what designs other people came up with.



Me too.

And: I would really like another version of current_schema() that is 
resolved/bound at the moment the function is defined.



  I just want to keep them in my own secret schema. [remember POSTGIS?]

Secretive for having security through obscurity?  But you can't really
hide schema information when users still need access to system catalogs.
But you can decide to revoke EXECUTE privilege from those functions and
give users a few SECURITY DEFINER functions as entry points to the
"private" parts of the schema.


It is not about security. security is orthogonal to this.

And the SECURITY DEFINER is already present.

The factory function will generate a function in the end-users schema

, but only if this end-user has sufficient rights.


Anything special about PostGIS in this regard?  In my databases PostGIS
either lives in public or a dedicated schema.  But there's nothing
secretive about it.
IIRC postgis needs to be in the search_path, or it will suffer the same 
restrictions.



HTH,

AvK





Re: Indexing fragments of a column's value ?

2023-11-14 Thread dld



You can index on expressions, and these will be recognised by the query 
generator.


drop schema tmp CASCADE;
create schema tmp;
set search_path = tmp;

CREATE TABLE bagger
    ( eight CHAR(8) NOT NULL PRIMARY KEY
    , more text
    );
CREATE INDEX bagger_idx_12 ON bagger (substr(eight,1,2));
CREATE INDEX bagger_idx_34 ON bagger (substr(eight,3,2));
CREATE INDEX bagger_idx_58 ON bagger (substr(eight,5,4));

INSERT INTO bagger(eight, more)
SELECT translate(to_hex( gs), ' ' , '0')
    , gs::text
FROM generate_series(0,40, 64999) gs
    ;

VACUUM ANALYZE bagger;

EXPLAIN ANALYZE
SELECT * FROM bagger
WHERE 1=1
    -- AND eight >= '00' AND eight < '05'
    AND substr(eight, 1,2) >= '30' AND substr(eight,1,2) < '05'
    AND substr(eight, 3,2) >= '90' AND substr(eight,3,2) < 'A5'
    AND substr(eight, 5,4) >= '' AND substr(eight,5,4) < ''
    ;

/*** The optimiser is smart enough to ignore one of the indexes. */
QUERY PLAN
-
 Bitmap Heap Scan on bagger  (cost=41.46..59.62 rows=1 width=19) 
(actual time=0.049..0.056 rows=0 loops=1)
   Recheck Cond: ((substr((eight)::text, 1, 2) >= '30'::text) AND 
(substr((eight)::text, 1, 2) < '05'::text) AND (substr((eight)::text, 3, 
2) >= '90'::text) AND (substr((eight)::text, 3, 2) < 'A5'::text))
   Filter: ((substr((eight)::text, 5, 4) >= ''::text) AND 
(substr((eight)::text, 5, 4) < ''::text))
   ->  BitmapAnd  (cost=41.46..41.46 rows=16 width=0) (actual 
time=0.042..0.047 rows=0 loops=1)
 ->  Bitmap Index Scan on bagger_idx_12 (cost=0.00..4.47 
rows=308 width=0) (actual time=0.039..0.039 rows=0 loops=1)
   Index Cond: ((substr((eight)::text, 1, 2) >= '30'::text) 
AND (substr((eight)::text, 1, 2) < '05'::text))
 ->  Bitmap Index Scan on bagger_idx_34 (cost=0.00..36.74 
rows=3205 width=0) (never executed)
   Index Cond: ((substr((eight)::text, 3, 2) >= '90'::text) 
AND (substr((eight)::text, 3, 2) < 'A5'::text))

 Planning Time: 5.487 ms
 Execution Time: 0.310 ms
(10 rows)


HTH,

AvK