Skip to content

Commit

Permalink
Merge pull request #165 from okbob/pragma-check
Browse files Browse the repository at this point in the history
Pragma check
  • Loading branch information
okbob authored Dec 9, 2023
2 parents 41c9492 + c2c1dfb commit 831fe94
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,12 @@ Shorter syntax for pragma is supported too:

* `sequence: name` - create ephemeral temporary sequence

* `assert-schema: varname` - check-time assertation - ensure so schema specified by variable is valid

* `assert-table: [ varname_schema, ] , varname` - ensure so table name specified by variables (by constant tracing) is valid

* `assert-column: [varname_schema, ], varname_table , varname` - ensure so column spefified by variables is valid

Pragmas `enable:tracer` and `disable:tracer`are active for Postgres 12 and higher

# Update
Expand Down
42 changes: 42 additions & 0 deletions expected/plpgsql_check_active.out
Original file line number Diff line number Diff line change
Expand Up @@ -9158,3 +9158,45 @@ end;
$$;
set plpgsql_check.strict_cursors_leaks to off;
drop function fx();
create table public.testt(a int, b int);
create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'a';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-schema: v1';
perform 'pragma:assert-table: v1, v2';
perform 'pragma:assert-column: v1, v2, v3';
perform 'pragma:assert-table: v2';
perform 'pragma:assert-column: v2, v3';
end;
$$ language plpgsql;
select * from plpgsql_check_function('fx()');
plpgsql_check_function
------------------------
(0 rows)

create table public.testt(a int, b int);
ERROR: relation "testt" already exists
create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'x';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-column: v1, v2, v3';
end;
$$ language plpgsql;
select * from plpgsql_check_function('fx()');
plpgsql_check_function
------------------------------------------------------------------------------
error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist
(1 row)

drop function fx();
drop table public.testt;
42 changes: 42 additions & 0 deletions expected/plpgsql_check_active_1.out
Original file line number Diff line number Diff line change
Expand Up @@ -9161,3 +9161,45 @@ end;
$$;
set plpgsql_check.strict_cursors_leaks to off;
drop function fx();
create table public.testt(a int, b int);
create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'a';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-schema: v1';
perform 'pragma:assert-table: v1, v2';
perform 'pragma:assert-column: v1, v2, v3';
perform 'pragma:assert-table: v2';
perform 'pragma:assert-column: v2, v3';
end;
$$ language plpgsql;
select * from plpgsql_check_function('fx()');
plpgsql_check_function
------------------------
(0 rows)

create table public.testt(a int, b int);
ERROR: relation "testt" already exists
create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'x';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-column: v1, v2, v3';
end;
$$ language plpgsql;
select * from plpgsql_check_function('fx()');
plpgsql_check_function
------------------------------------------------------------------------------
error:42703:8:PERFORM:column "x" of relation "public"."testt" does not exist
(1 row)

drop function fx();
drop table public.testt;
40 changes: 40 additions & 0 deletions sql/plpgsql_check_active.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5411,3 +5411,43 @@ $$;
set plpgsql_check.strict_cursors_leaks to off;

drop function fx();

create table public.testt(a int, b int);

create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'a';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-schema: v1';
perform 'pragma:assert-table: v1, v2';
perform 'pragma:assert-column: v1, v2, v3';
perform 'pragma:assert-table: v2';
perform 'pragma:assert-column: v2, v3';
end;
$$ language plpgsql;

select * from plpgsql_check_function('fx()');

create table public.testt(a int, b int);

create or replace function fx()
returns void as $$
declare
v1 varchar default 'public';
v2 varchar default 'testt';
v3 varchar default 'x';
begin
raise notice '%', format('%I.%I.%I', v1, v2, v3);
perform 'pragma:assert-column: v1, v2, v3';
end;
$$ language plpgsql;

select * from plpgsql_check_function('fx()');

drop function fx();

drop table public.testt;
182 changes: 181 additions & 1 deletion src/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,186 @@ get_name(List *names)
return sinfo.data;
}

static const char *
pragma_assert_name(PragmaAssertType pat)
{
switch (pat)
{
case PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA:
return "assert-schema";
case PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE:
return "assert-table";
case PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN:
return "assert-column";
}

return NULL;
}

static Oid
check_var_schema(PLpgSQL_checkstate *cstate, int dno)
{
return get_namespace_oid(cstate->strconstvars[dno], true);
}

static Oid
check_var_table(PLpgSQL_checkstate *cstate, int dno1, int dno2)
{
char *relname = cstate->strconstvars[dno2];
Oid relid = InvalidOid;

if (dno1 != -1)
relid = get_relname_relid(relname, check_var_schema(cstate, dno1));
else
relid = RelnameGetRelid(relname);

if (!OidIsValid(relid))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_TABLE),
errmsg("table \"%s\" does not exist", relname)));

return relid;
}

static AttrNumber
check_var_column(PLpgSQL_checkstate *cstate, int dno1, int dno2, int dno3)
{
char *attname = cstate->strconstvars[dno3];
Oid relid = check_var_table(cstate, dno1, dno2);
AttrNumber attnum;

attnum = get_attnum(relid, attname);
if (attnum == InvalidAttrNumber)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
attname,
get_namespace_name(get_rel_namespace(relid)),
get_rel_name(relid))));

return attnum;
}

bool
plpgsql_check_pragma_assert(PLpgSQL_checkstate *cstate,
PragmaAssertType pat,
const char *str,
PLpgSQL_nsitem *ns,
int lineno)
{
MemoryContext oldCxt;
ResourceOwner oldowner;
volatile int dno[3];
volatile int nvars = 0;
volatile bool result = true;

/*
* namespace is available only in compile check mode, and only in this mode
* this pragma can be used.
*/
if (!ns || !cstate)
return true;

oldCxt = CurrentMemoryContext;

oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(cstate->check_cxt);

PG_TRY();
{
TokenizerState tstate;
int i;
List *names;

initialize_tokenizer(&tstate, str);

for (i = 0; i < 3; i++)
{
if (i > 0)
{
PragmaTokenType token, *_token;

_token = get_token(&tstate, &token);
if (_token->value != ',')
elog(ERROR, "Syntax error (expected \",\")");
}

names = get_qualified_identifier(&tstate, NULL);
if ((dno[i] = get_varno(ns, names)) == -1)
elog(ERROR, "Cannot to find variable %s used in \"%s\" pragma",
get_name(names),
pragma_assert_name(pat));

if (!cstate->strconstvars || !cstate->strconstvars[dno[i]])
elog(ERROR, "Variable %s has not assigned constant",
get_name(names));

nvars += 1;

if (tokenizer_eol(&tstate))
break;
}

if (!tokenizer_eol(&tstate))
elog(ERROR, "Syntax error (unexpected chars after variable)");

if ((pat == PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA && nvars > 1) ||
(pat == PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE && nvars > 2) ||
(pat == PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN && nvars > 3))
elog(ERROR, "too much variables for \"%s\" pragma",
pragma_assert_name(pat));

RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(oldCxt);
CurrentResourceOwner = oldowner;
}
PG_CATCH();
{
ErrorData *edata;

MemoryContextSwitchTo(cstate->check_cxt);
edata = CopyErrorData();
FlushErrorState();

MemoryContextSwitchTo(oldCxt);
FlushErrorState();

RollbackAndReleaseCurrentSubTransaction();
MemoryContextSwitchTo(oldCxt);
CurrentResourceOwner = oldowner;

/* raise warning (errors in pragma can be ignored instead */
ereport(WARNING,
(errmsg("\"%s\" on line %d is not processed.", pragma_assert_name(pat), lineno),
errdetail("%s", edata->message)));

result = false;
}
PG_END_TRY();

if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA)
{
(void) check_var_schema(cstate, dno[0]);
}
else if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE)
{
if (nvars == 1)
(void) check_var_table(cstate, -1, dno[0]);
else
(void) check_var_table(cstate, dno[0], dno[1]);
}
else if (pat == PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN)
{
if (nvars == 2)
(void) check_var_column(cstate, -1, dno[0], dno[1]);
else
(void) check_var_column(cstate, dno[0], dno[1], dno[2]);
}

return result;
}

bool
plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate,
const char *str,
Expand Down Expand Up @@ -930,7 +1110,7 @@ plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate,

names = get_qualified_identifier(&tstate, NULL);
if ((target_dno = get_varno(ns, names)) == -1)
elog(ERROR, "Cannot to find variable \"%s\" used in settype pragma", get_name(names));
elog(ERROR, "Cannot to find variable %s used in settype pragma", get_name(names));

target = cstate->estate->datums[target_dno];
if (target->dtype != PLPGSQL_DTYPE_REC)
Expand Down
10 changes: 10 additions & 0 deletions src/plpgsql_check.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ typedef enum
PLPGSQL_CHECK_STMT_WALKER_COLLECT_COVERAGE
} profiler_stmt_walker_mode;

typedef enum
{
PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA,
PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE,
PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN,
} PragmaAssertType;


typedef struct PLpgSQL_stmt_stack_item
{
PLpgSQL_stmt *stmt;
Expand Down Expand Up @@ -323,6 +331,8 @@ extern Oid plpgsql_check_parse_name_or_signature(char *name_or_signature);
extern bool plpgsql_check_pragma_type(PLpgSQL_checkstate *cstate, const char *str, PLpgSQL_nsitem *ns, int lineno);
extern bool plpgsql_check_pragma_table(PLpgSQL_checkstate *cstate, const char *str, int lineno);
extern bool plpgsql_check_pragma_sequence(PLpgSQL_checkstate *cstate, const char *str, int lineno);
extern bool plpgsql_check_pragma_assert(PLpgSQL_checkstate *cstate, PragmaAssertType pat, const char *str,
PLpgSQL_nsitem *ns, int lineno);
extern void plpgsql_check_search_comment_options(plpgsql_check_info *cinfo);
extern char *plpgsql_check_process_echo_string(char *str, plpgsql_check_info *cinfo);

Expand Down
18 changes: 18 additions & 0 deletions src/pragma.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,24 @@ pragma_apply(PLpgSQL_checkstate *cstate,
{
is_valid = plpgsql_check_pragma_sequence(cstate, pragma_str + 9, lineno);
}
else if (strncasecmp(pragma_str, "ASSERT-SCHEMA:", 14) == 0)
{
is_valid = plpgsql_check_pragma_assert(cstate,
PLPGSQL_CHECK_PRAGMA_ASSERT_SCHEMA,
pragma_str + 14, ns, lineno);
}
else if (strncasecmp(pragma_str, "ASSERT-TABLE:", 13) == 0)
{
is_valid = plpgsql_check_pragma_assert(cstate,
PLPGSQL_CHECK_PRAGMA_ASSERT_TABLE,
pragma_str + 13, ns, lineno);
}
else if (strncasecmp(pragma_str, "ASSERT-COLUMN:", 14) == 0)
{
is_valid = plpgsql_check_pragma_assert(cstate,
PLPGSQL_CHECK_PRAGMA_ASSERT_COLUMN,
pragma_str + 14, ns, lineno);
}
else
{
elog(WARNING, "unsupported pragma: %s", pragma_str);
Expand Down

0 comments on commit 831fe94

Please sign in to comment.