Procedural and security objects

A model target can describe more than tables and views:

  • routines: functions and procedures stored in a schema;
  • triggers: on a table, calling a routine (PostgreSQL) or carrying an inline body (MySQL, SQL Server, Oracle);
  • materialized views (indexed views on SQL Server), with their own indexes;
  • partitioning of a table by range, list or hash, with named partitions and verbatim bounds;
  • roles at the target level, with login, membership and options;
  • grants of privileges on a schema, on all tables / sequences / routines of a schema, or on one object.

All of them are physical: no logical object is derived for them, and bluep pull applies them to the target only.

Text format

target_decl    = "target" , name , ":" , dialect , block_open , { ( role_decl | schema_decl ) , nl } , block_close ;
role_decl      = "role" , name , block_open , { role_item , nl } , block_close ;
role_item      = "login" | "member_of" , name | "option" , name , string ;
schema_item    += routine_decl | matview_decl | grant_decl ;
routine_decl   = ( "function" | "procedure" ) , name , "(" , [ arg , { "," , arg } ] , ")" ,
                 block_open , { routine_item , nl } , block_close ;
arg            = [ "in" | "out" | "inout" ] , [ name ] , phys_type , [ "default" , string ] ;
routine_item   = "returns" , string | "language" , name | "option" , name , string | "comment" , string
               | prop_item | "body" , raw_string ;
matview_decl   = "materialized_view" , name , block_open , { matview_item , nl } , block_close ;
matview_item   = "comment" , string | prop_item | "no" , "data" | "sql" , raw_string
               | "index" , name , [ "unique" ] , "(" , index_col , { "," , index_col } , ")" , [ "using" , name ] , [ "where" , string ] ;
grant_decl     = "grant" , privilege , { "," , privilege } , "on" , grant_object , "to" , name , [ "with" , "grant" , "option" ] ;
privilege      = "select" | "insert" | "update" | "delete" | "truncate" | "references" | "trigger" | "usage"
               | "execute" | "create" | "all" ;
grant_object   = "schema" | "all" , "tables" | "all" , "sequences" | "all" , "routines"
               | "table" , name | "view" , name | "sequence" , name | "routine" , name ;
table_item     += trigger_decl | partition_decl ;
trigger_decl   = "trigger" , name , block_open , { trigger_item , nl } , block_close ;
trigger_item   = "timing" , ( "before" | "after" | "instead_of" ) | "events" , event , { "," , event }
               | "update_columns" , "(" , name_list , ")" | "for" , "each" , ( "row" | "statement" )
               | "when" , string | "function" , name | "comment" , string | "body" , raw_string ;
event          = "insert" | "update" | "delete" | "truncate" ;
partition_decl = "partition" , "by" , ( "range" | "list" | "hash" ) , "(" , index_col , { "," , index_col } , ")" ,
                 block_open , { "partition" , name , string , nl } , block_close ;

An argument is read as [mode] [name] type: when two words (or strings) precede ,, ) or default, the first is the name and the second the type; with one word it is the type. A mode word with no type after it is the error expected argument type.

Canonical order

Block Item order
target prop*, role* (by name), schema* (by name)
role login (only when true), member_of*, option* (by key)
schema comment, prop*, enum_type*, sequence*, table*, view*, materialized_view*, routines (function and procedure together, by name), grant* (declaration order)
table entity, comment, classify*, prop*, option*, partition by, column*, primary key, unique*, check*, foreign key*, index*, trigger* (by name)
routine returns, language, option* (by key), comment, prop*, body (always written)
materialized view comment, prop*, no data (only when not populated), sql, index* (by name)
trigger timing, events, update_columns (when any), for each statement (when statement level), when, function, comment, body
partition block partition <name> "<bound>" per partition, in declaration order

Privileges are written in the order select insert update delete truncate references trigger usage execute create all; the grantee public is written bare.

Generated SQL by dialect

PostgreSQL (dollar quoting picks $$, then $bluep$, $bluep1$, ... so that the tag never occurs in the body):

CREATE FUNCTION public.set_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  RETURN NEW;
END;
$$;
CREATE TRIGGER trg_order_touch BEFORE UPDATE ON public."order" FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();

MySQL (routines and triggers are written verbatim inside a DELIMITER block):

DELIMITER $$
CREATE FUNCTION customer_count(min_id BIGINT) RETURNS BIGINT DETERMINISTIC
BEGIN
  RETURN (SELECT COUNT(*) FROM customer WHERE id >= min_id);
END$$
DELIMITER ;
DELIMITER $$
CREATE TRIGGER trg_customer_touch BEFORE UPDATE ON customer FOR EACH ROW
BEGIN
  SET NEW.updated_at = NOW(6);
END$$
DELIMITER ;

SQL Server (each routine and trigger is its own batch between GO lines):

GO
CREATE FUNCTION app.customer_count(@min_id BIGINT) RETURNS bigint AS
BEGIN
  RETURN (SELECT COUNT(*) FROM app.customer WHERE id >= @min_id);
END
GO
CREATE TRIGGER app.trg_customer_touch ON app.customer AFTER UPDATE AS
BEGIN
  UPDATE c SET updated_at = SYSUTCDATETIME() FROM app.customer c INNER JOIN inserted i ON i.id = c.id;
END
GO

Oracle (PL/SQL blocks end with a / line; routine names are quoted to keep their case):

CREATE OR REPLACE FUNCTION "app"."customer_count"(min_id IN NUMBER DEFAULT 0) RETURN NUMBER AS
n NUMBER;
BEGIN
  SELECT COUNT(*) INTO n FROM app.customer WHERE id >= min_id;
  RETURN n;
END;
/
CREATE OR REPLACE TRIGGER trg_customer_touch BEFORE UPDATE OF name ON app.customer FOR EACH ROW WHEN (OLD.name <> NEW.name)
BEGIN
  :NEW.updated_at := SYSTIMESTAMP;
END;
/

SQLite and the cloud warehouse dialects skip every procedural object with a note such as function 'f' skipped: not supported by SQLite.

Compare and migrate

bluep diff reports + function s.f, ~ function s.f: replaced, + trigger s.t.trg, ~ partitioning s.t: range (a) -> none, + role r, ~ role r: altered, + grant s: <grant> and the matching drops. A comment-only change is a comment change. bluep migrate orders the statements so that triggers and grants are dropped first and grants are created last.

Change PostgreSQL MySQL SQL Server Oracle
AddRoutine create create create create
DropRoutine DROP FUNCTION <q> / DROP PROCEDURE same same same
ReplaceRoutine CREATE OR REPLACE when kind, argument types and return type are unchanged, else drop + add drop + add drop + add CREATE OR REPLACE
AddTrigger create create create create
DropTrigger DROP TRIGGER <name> ON <table> DROP TRIGGER <q> DROP TRIGGER <q> DROP TRIGGER <q>
ReplaceTrigger drop + add drop + add drop + add CREATE OR REPLACE
AddMaterializedView create skipped create create
DropMaterializedView DROP MATERIALIZED VIEW <q> skipped DROP VIEW <q> DROP MATERIALIZED VIEW <q>
ReplaceMaterializedView drop + add skipped drop + add drop + add
AddPartition CREATE TABLE <p> PARTITION OF <t> FOR VALUES <bound> ALTER TABLE <t> ADD PARTITION (PARTITION <p> <bound>) skipped ALTER TABLE <t> ADD PARTITION <p> <bound>
DropPartition DETACH PARTITION then DROP TABLE ALTER TABLE <t> DROP PARTITION <p> skipped ALTER TABLE <t> DROP PARTITION <p>
SetPartitioning skipped skipped skipped skipped
AddRole / DropRole CREATE ROLE + memberships / DROP ROLE same same same
AlterRole ALTER ROLE ... LOGIN | NOLOGIN ... + membership changes memberships only memberships only memberships only
AddGrant / RevokeGrant GRANT / REVOKE same same same
comment on routine, materialized view, trigger COMMENT ON ... skipped skipped skipped

A change the dialect cannot express is listed at the top of the script as -- skipped unsupported: <change>.

Import

bluep import ddl reads routines, triggers, materialized views, PARTITION BY / PARTITION OF / ATTACH PARTITION, roles, role memberships, GRANT and REVOKE for PostgreSQL, MySQL (including DELIMITER blocks), SQL Server (GO batches) and Oracle (/ lines).

bluep import db reads the same objects from live catalogs:

Object PostgreSQL MySQL
routines pg_get_functiondef for pg_proc rows of kind f and p, excluding extension members information_schema.ROUTINES joined to PARAMETERS
triggers pg_get_triggerdef for non-internal triggers information_schema.TRIGGERS
materialized views pg_matviews, their indexes from pg_indexes not available
partitions pg_partitioned_table, pg_get_partkeydef, pg_inherits and relpartbound information_schema.PARTITIONS
roles pg_roles (optional) and pg_auth_members (optional) mysql.user locked, expired accounts (optional) and mysql.role_edges (optional)
grants information_schema.role_table_grants (owner excluded) and schema USAGE from the schema ACL information_schema.TABLE_PRIVILEGES

A failing optional query yields no rows and the note <client> query '<id>' failed: <first line of stderr>.