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>.