feat(hirearchy): add helpers
This commit is contained in:
+73
-15
@@ -322,21 +322,72 @@ SELECT * FROM morbac.compile_policy();
|
|||||||
|
|
||||||
### Organization Rule Scope
|
### Organization Rule Scope
|
||||||
|
|
||||||
Rules in pgmorbac are always scoped to an organization (`org_id`).
|
Rules in pgmorbac are always scoped to a single organization (`org_id`). To apply a rule across multiple organizations in a hierarchy, use `get_org_scope(org_id, scope)`, which returns a set of `(org_id, depth)` rows for the named scope relative to the given org.
|
||||||
|
|
||||||
- To apply a rule only to a specific org, insert it with that `org_id`.
|
| Scope | Returns |
|
||||||
- To apply a rule to all descendants, use `get_org_descendants(org_id)`:
|
|---|---|
|
||||||
```sql
|
| `'self'` | The org itself only |
|
||||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
| `'children'` | Direct children only (depth = 1) |
|
||||||
SELECT descendant_id, :role_id, :activity, :view, 'permission'
|
| `'descendants'` | All descendants, excluding self |
|
||||||
FROM morbac.get_org_descendants(:org_id)
|
| `'subtree'` | Self + all descendants |
|
||||||
UNION ALL
|
| `'parent'` | Direct parent only |
|
||||||
SELECT :org_id, :role_id, :activity, :view, 'permission';
|
| `'ancestors'` | All ancestors, excluding self |
|
||||||
```
|
| `'lineage'` | Self + all ancestors |
|
||||||
- To reference parent orgs, use `get_org_ancestors(org_id)`.
|
| `'root'` | Topmost ancestor only |
|
||||||
- For cross-organization permissions or admin delegation, use `cross_org_rules` and `admin_rules`.
|
|
||||||
|
|
||||||
See API Reference for details on these helper functions and tables.
|
An optional third argument `p_max_depth` limits how many levels are traversed.
|
||||||
|
|
||||||
|
**Example: Entire subtree (org + all subsidiaries)**
|
||||||
|
|
||||||
|
A holding company grants its auditor role read access across all subsidiaries:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
||||||
|
SELECT org_id, :role_id, 'read', 'financials', 'permission'
|
||||||
|
FROM morbac.get_org_scope(:holding_org_id, 'subtree');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Direct children only**
|
||||||
|
|
||||||
|
A regional manager role applies only to first-level divisions, not deeper sub-divisions:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
||||||
|
SELECT org_id, :role_id, 'manage', 'teams', 'permission'
|
||||||
|
FROM morbac.get_org_scope(:region_org_id, 'children');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Two levels deep**
|
||||||
|
|
||||||
|
A policy that covers a org, its direct children, and their children:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
||||||
|
SELECT org_id, :role_id, 'read', 'documents', 'permission'
|
||||||
|
FROM morbac.get_org_scope(:org_id, 'subtree', 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Root org only**
|
||||||
|
|
||||||
|
A compliance rule attached to the top-level org, regardless of where you start:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
||||||
|
SELECT org_id, :role_id, 'audit', 'all_data', 'permission'
|
||||||
|
FROM morbac.get_org_scope(:any_child_org_id, 'root');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: All ancestors (upward propagation)**
|
||||||
|
|
||||||
|
A report created in a child org becomes visible to all parent orgs:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.rules (org_id, role_id, activity, view, modality)
|
||||||
|
SELECT org_id, :role_id, 'read', 'reports', 'permission'
|
||||||
|
FROM morbac.get_org_scope(:child_org_id, 'ancestors');
|
||||||
|
```
|
||||||
|
|
||||||
|
For cross-organization access between unrelated orgs, use `cross_org_rules` and `admin_rules`.
|
||||||
|
|
||||||
### Hierarchies
|
### Hierarchies
|
||||||
|
|
||||||
@@ -613,9 +664,16 @@ SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
|
|||||||
|
|
||||||
### Hierarchy Functions
|
### Hierarchy Functions
|
||||||
|
|
||||||
**`get_org_ancestors(org_id)`**: Returns all parent organizations with depth.
|
**`get_org_ancestors(org_id)`**: Returns all parent organizations with depth (including self at depth 0).
|
||||||
|
|
||||||
**`get_org_descendants(org_id)`**: Returns all child organizations with depth.
|
**`get_org_descendants(org_id)`**: Returns all child organizations with depth (including self at depth 0).
|
||||||
|
|
||||||
|
**`get_org_scope(org_id, scope, max_depth?)`**: Returns a named set of organizations relative to `org_id`. Scope values: `self`, `children`, `descendants`, `subtree`, `parent`, `ancestors`, `lineage`, `root`. Optional `max_depth` limits traversal depth.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- All orgs in the subtree, up to 2 levels deep
|
||||||
|
SELECT * FROM morbac.get_org_scope(org_uuid, 'subtree', 2);
|
||||||
|
```
|
||||||
|
|
||||||
**`get_inherited_roles(role_id)`**: Returns all junior roles (transitive).
|
**`get_inherited_roles(role_id)`**: Returns all junior roles (transitive).
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,92 @@ $$;
|
|||||||
COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS
|
COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS
|
||||||
'Returns all descendant organizations (including self) with depth in hierarchy';
|
'Returns all descendant organizations (including self) with depth in hierarchy';
|
||||||
|
|
||||||
|
-- Get a named scope of organizations relative to a given org
|
||||||
|
--
|
||||||
|
-- Supported scopes:
|
||||||
|
-- 'self' — the org itself only (depth = 0)
|
||||||
|
-- 'children' — direct children only (descendants at depth = 1)
|
||||||
|
-- 'descendants' — all descendants, excluding self (depth > 0)
|
||||||
|
-- 'subtree' — self + all descendants (equivalent to get_org_descendants)
|
||||||
|
-- 'parent' — direct parent only (ancestor at depth = 1)
|
||||||
|
-- 'ancestors' — all ancestors, excluding self (depth > 0)
|
||||||
|
-- 'lineage' — self + all ancestors (equivalent to get_org_ancestors)
|
||||||
|
-- 'root' — topmost ancestor only (max depth ancestor)
|
||||||
|
--
|
||||||
|
-- Optional p_max_depth limits how many levels are traversed (NULL = unlimited).
|
||||||
|
-- For upward scopes (parent/ancestors/lineage/root) depth counts steps toward root.
|
||||||
|
-- For downward scopes (children/descendants/subtree) depth counts steps toward leaves.
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.get_org_scope(
|
||||||
|
p_org_id UUID,
|
||||||
|
p_scope TEXT,
|
||||||
|
p_max_depth INTEGER DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE(org_id UUID, depth INTEGER)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
CASE p_scope
|
||||||
|
WHEN 'self' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT p_org_id, 0;
|
||||||
|
|
||||||
|
WHEN 'children' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT d.org_id, d.depth
|
||||||
|
FROM morbac.get_org_descendants(p_org_id) d
|
||||||
|
WHERE d.depth = 1
|
||||||
|
AND (p_max_depth IS NULL OR d.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'descendants' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT d.org_id, d.depth
|
||||||
|
FROM morbac.get_org_descendants(p_org_id) d
|
||||||
|
WHERE d.depth > 0
|
||||||
|
AND (p_max_depth IS NULL OR d.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'subtree' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT d.org_id, d.depth
|
||||||
|
FROM morbac.get_org_descendants(p_org_id) d
|
||||||
|
WHERE (p_max_depth IS NULL OR d.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'parent' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT a.org_id, a.depth
|
||||||
|
FROM morbac.get_org_ancestors(p_org_id) a
|
||||||
|
WHERE a.depth = 1
|
||||||
|
AND (p_max_depth IS NULL OR a.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'ancestors' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT a.org_id, a.depth
|
||||||
|
FROM morbac.get_org_ancestors(p_org_id) a
|
||||||
|
WHERE a.depth > 0
|
||||||
|
AND (p_max_depth IS NULL OR a.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'lineage' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT a.org_id, a.depth
|
||||||
|
FROM morbac.get_org_ancestors(p_org_id) a
|
||||||
|
WHERE (p_max_depth IS NULL OR a.depth <= p_max_depth);
|
||||||
|
|
||||||
|
WHEN 'root' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT a.org_id, a.depth
|
||||||
|
FROM morbac.get_org_ancestors(p_org_id) a
|
||||||
|
ORDER BY a.depth DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'get_org_scope: unknown scope "%". Valid scopes: self, children, descendants, subtree, parent, ancestors, lineage, root', p_scope;
|
||||||
|
END CASE;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION morbac.get_org_scope(UUID, TEXT, INTEGER) IS
|
||||||
|
'Returns a named set of organizations relative to p_org_id. Scopes: self, children, descendants, subtree, parent, ancestors, lineage, root. Optional p_max_depth limits traversal depth.';
|
||||||
|
|
||||||
-- Get all roles a user effectively has (direct + inherited via role hierarchy)
|
-- Get all roles a user effectively has (direct + inherited via role hierarchy)
|
||||||
CREATE OR REPLACE FUNCTION morbac.get_effective_roles(p_user_id UUID, p_org_id UUID)
|
CREATE OR REPLACE FUNCTION morbac.get_effective_roles(p_user_id UUID, p_org_id UUID)
|
||||||
RETURNS TABLE(role_id UUID, depth INTEGER)
|
RETURNS TABLE(role_id UUID, depth INTEGER)
|
||||||
|
|||||||
@@ -361,5 +361,163 @@ SELECT morbac.t('Dave (GlobalTech employee) reads Engineering docs [no cross-org
|
|||||||
'read', 'documents'
|
'read', 'documents'
|
||||||
), FALSE);
|
), FALSE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 6: get_org_scope — named scope helper
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Org tree used in tests:
|
||||||
|
-- GlobalTech HQ (root) id: 10000000-0000-0000-0000-000000000001
|
||||||
|
-- ├── Engineering Dept id: 10000000-0000-0000-0000-000000000002
|
||||||
|
-- └── Sales Dept id: 10000000-0000-0000-0000-000000000003
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 6. get_org_scope ---'
|
||||||
|
|
||||||
|
-- 'self' — always returns exactly the org itself
|
||||||
|
SELECT morbac.t_eq('scope self (root) returns 1 row',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'self'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope self (leaf) returns 1 row',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'self'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope self returns the org itself at depth 0',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'self')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 0
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- 'children' — direct children only (depth = 1 descendants)
|
||||||
|
SELECT morbac.t_eq('scope children of root returns 2 rows (Engineering + Sales)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'children'))::bigint, 2);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope children of leaf returns 0 rows',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'children'))::bigint, 0);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope children does not include self (root)',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'children')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope children includes Engineering at depth 1',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'children')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 1
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- 'descendants' — all descendants excluding self
|
||||||
|
SELECT morbac.t_eq('scope descendants of root returns 2 rows (Engineering + Sales, no self)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'descendants'))::bigint, 2);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope descendants does not include self',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'descendants')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope descendants of leaf returns 0 rows',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'descendants'))::bigint, 0);
|
||||||
|
|
||||||
|
-- 'subtree' — self + all descendants
|
||||||
|
SELECT morbac.t_eq('scope subtree of root returns 3 rows (self + Engineering + Sales)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree'))::bigint, 3);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope subtree includes self at depth 0',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001' AND depth = 0
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope subtree of leaf returns 1 row (self only)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'subtree'))::bigint, 1);
|
||||||
|
|
||||||
|
-- 'parent' — direct parent only
|
||||||
|
SELECT morbac.t_eq('scope parent of Engineering returns 1 row (GlobalTech HQ)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'parent'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope parent of Engineering returns GlobalTech HQ at depth 1',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'parent')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001' AND depth = 1
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope parent of root returns 0 rows (no parent)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'parent'))::bigint, 0);
|
||||||
|
|
||||||
|
-- 'ancestors' — all ancestors excluding self
|
||||||
|
SELECT morbac.t_eq('scope ancestors of Engineering returns 1 row (GlobalTech HQ only)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'ancestors'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope ancestors does not include self',
|
||||||
|
NOT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'ancestors')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000002'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope ancestors of root returns 0 rows',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'ancestors'))::bigint, 0);
|
||||||
|
|
||||||
|
-- 'lineage' — self + all ancestors
|
||||||
|
SELECT morbac.t_eq('scope lineage of Engineering returns 2 rows (self + GlobalTech HQ)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'lineage'))::bigint, 2);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope lineage includes self at depth 0',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'lineage')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 0
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope lineage of root returns 1 row (self only)',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'lineage'))::bigint, 1);
|
||||||
|
|
||||||
|
-- 'root' — topmost ancestor only
|
||||||
|
SELECT morbac.t_eq('scope root of Engineering returns 1 row',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'root'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope root of Engineering returns GlobalTech HQ',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'root')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope root of root returns the root itself',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'root'))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t('scope root of root returns the org itself',
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'root')
|
||||||
|
WHERE org_id = '10000000-0000-0000-0000-000000000001'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- p_max_depth — depth limiting
|
||||||
|
SELECT morbac.t_eq('scope subtree max_depth=0 returns only self',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree', 0))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope subtree max_depth=1 returns self + direct children',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree', 1))::bigint, 3);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope descendants max_depth=1 returns only direct children',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'descendants', 1))::bigint, 2);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope lineage max_depth=0 returns only self',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'lineage', 0))::bigint, 1);
|
||||||
|
|
||||||
|
SELECT morbac.t_eq('scope ancestors max_depth=1 returns only direct parent',
|
||||||
|
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'ancestors', 1))::bigint, 1);
|
||||||
|
|
||||||
|
-- Unknown scope raises an error
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'invalid_scope');
|
||||||
|
RAISE EXCEPTION 'CHECK FAIL: unknown scope should have raised an error';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%unknown scope%' THEN
|
||||||
|
RAISE NOTICE 'CHECK PASS: unknown scope raises error as expected';
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'CHECK FAIL: unexpected error: %', SQLERRM;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
\echo ''
|
\echo ''
|
||||||
\echo '=== Hierarchy Tests Completed ==='
|
\echo '=== Hierarchy Tests Completed ==='
|
||||||
|
|||||||
Reference in New Issue
Block a user