pg_tviews 0.1.0-beta.12

Transactional materialized views with incremental refresh for PostgreSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# Security Guide

**Version**: 0.1.0-beta.1
**Last Updated**: December 11, 2025

## Overview

pg_tviews provides powerful SQL generation capabilities that require careful security considerations. This guide covers SQL injection prevention, access control, and secure usage patterns.

## SQL Injection Prevention

### Safe Parameter Handling

**✅ SAFE: Function parameters are escaped**
```sql
SELECT pg_tviews_create('tv_post', $$
  SELECT
    tb_post.pk_post,  -- INTEGER pk
    tb_post.id,       -- UUID
    jsonb_build_object(
      'id', tb_post.id,
      'title', tb_post.title,
      'user_id', tb_user.id
    ) as data
  FROM tb_post
  INNER JOIN tb_user ON tb_post.fk_user = tb_user.pk_user
$$);
```

**❌ UNSAFE: Never concatenate user input**
```sql
-- DON'T DO THIS
SELECT pg_tviews_create(user_provided_name, user_provided_sql);
```

### Dynamic TVIEW Creation

When creating TVIEWs dynamically:

```sql
-- ✅ SAFE: Use format() with proper escaping
CREATE OR REPLACE FUNCTION create_user_posts_tview(user_uuid UUID)
RETURNS VOID AS $$
DECLARE
    tview_name TEXT;
BEGIN
    -- Safe name generation
    tview_name := format('tv_user_posts_%s', replace(user_uuid::TEXT, '-', '_'));

    -- Use parameterized queries
    EXECUTE format('SELECT pg_tviews_create(%L, %L)', tview_name, $$
        SELECT
          tb_post.pk_post,
          tb_post.id,
          jsonb_build_object('id', tb_post.id, 'title', tb_post.title) as data
        FROM tb_post WHERE tb_post.fk_user = (
          SELECT pk_user FROM tb_user WHERE id = $1
        )
    $$) USING user_uuid;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```

## Column-Level Security

### Excluding Sensitive Data

**❌ BAD: Including sensitive data**
```sql
CREATE TABLE tv_user AS
SELECT
  tb_user.pk_user,
  tb_user.id,
  jsonb_build_object(
    'id', tb_user.id,
    'username', tb_user.username,
    'password_hash', tb_user.password_hash  -- DON'T EXPOSE!
  ) as data
FROM tb_user;
```

**✅ GOOD: Exclude sensitive columns**
```sql
CREATE TABLE tv_user AS
SELECT
  tb_user.pk_user,
  tb_user.id,
  jsonb_build_object(
    'id', tb_user.id,
    'username', tb_user.username,
    'email', tb_user.email,
    'created_at', tb_user.created_at
  ) as data
FROM tb_user;
```

### Row-Level Security (RLS)

Implement RLS on TVIEWs for multi-tenant applications:

```sql
-- Enable RLS on TVIEW
ALTER TABLE tv_post ENABLE ROW LEVEL SECURITY;

-- Create security policy
CREATE POLICY tenant_posts ON tv_post
    FOR ALL
    USING (fk_user IN (
        SELECT pk_user FROM tb_user
        WHERE tenant_id = current_setting('app.tenant_id')::UUID
    ));

-- Create indexes to support RLS efficiently
CREATE INDEX idx_tv_post_fk_user_rls ON tv_post(fk_user);
```

## Access Control

### Granting Permissions

```sql
-- Grant read access to application user
GRANT SELECT ON tv_post TO app_user;
GRANT SELECT ON tv_user TO app_user;

-- Grant write access for data modifications
GRANT UPDATE ON tv_post TO app_admin;
GRANT INSERT, UPDATE, DELETE ON tb_post TO app_admin;

-- Grant TVIEW management permissions
GRANT EXECUTE ON FUNCTION pg_tviews_create(TEXT, TEXT) TO db_admin;
GRANT EXECUTE ON FUNCTION pg_tviews_drop(TEXT, BOOLEAN) TO db_admin;
```

### Role-Based Access

```sql
-- Create roles
CREATE ROLE readonly_user;
CREATE ROLE readwrite_user;
CREATE ROLE admin_user;

-- Grant appropriate permissions
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON tb_post, tb_user TO readwrite_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_user;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO admin_user;
```

## Data Validation

### Input Sanitization

```sql
-- ✅ SAFE: Validate UUID inputs
CREATE OR REPLACE FUNCTION get_user_posts_safe(user_id_param TEXT)
RETURNS TABLE (
    pk_post BIGINT,
    id UUID,
    data JSONB
) AS $$
BEGIN
    -- Validate input is valid UUID
    IF user_id_param !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN
        RAISE EXCEPTION 'Invalid UUID format';
    END IF;

    RETURN QUERY
    SELECT tv_post.pk_post, tv_post.id, tv_post.data
    FROM tv_post
    WHERE tv_post.fk_user = (
        SELECT pk_user FROM tb_user WHERE id = user_id_param::UUID
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```

### JSONB Data Validation

```sql
-- Validate JSONB structure before insertion
CREATE OR REPLACE FUNCTION validate_post_data()
RETURNS TRIGGER AS $$
BEGIN
    -- Check required fields exist
    IF NEW.data->>'id' IS NULL THEN
        RAISE EXCEPTION 'Post data must include id field';
    END IF;

    IF NEW.data->>'title' IS NULL OR trim(NEW.data->>'title') = '' THEN
        RAISE EXCEPTION 'Post data must include non-empty title field';
    END IF;

    -- Validate data types
    IF jsonb_typeof(NEW.data->'id') != 'string' THEN
        RAISE EXCEPTION 'Post id must be a string';
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Attach trigger to base table
CREATE TRIGGER validate_post_data_trigger
    BEFORE INSERT OR UPDATE ON tb_post
    FOR EACH ROW EXECUTE FUNCTION validate_post_data();
```

## Secure Configuration

### Connection Security

```sql
-- Use SSL connections
ALTER SYSTEM SET ssl = 'on';

-- Require SSL for pg_tviews operations
ALTER SYSTEM SET ssl_min_protocol_version = 'TLSv1.2';

-- Set secure search_path
ALTER DATABASE your_db SET search_path = 'public';
```

### Extension Security

```sql
-- Grant extension privileges carefully
GRANT CREATE ON SCHEMA public TO pg_tviews_user;

-- Don't grant superuser privileges
-- GRANT SUPERUSER TO pg_tviews_user;  -- DON'T DO THIS

-- Use SECURITY DEFINER for controlled access
CREATE FUNCTION pg_tviews_create_secure(tview_name TEXT, sql_query TEXT)
RETURNS VOID AS $$
BEGIN
    -- Add security checks here
    IF current_user != 'pg_tviews_admin' THEN
        RAISE EXCEPTION 'Access denied';
    END IF;

    -- Validate inputs
    IF tview_name !~ '^tv_[a-z_]+$' THEN
        RAISE EXCEPTION 'Invalid TVIEW name format';
    END IF;

    -- Call actual function
    PERFORM pg_tviews_create(tview_name, sql_query);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```

## Audit Logging

### Enable Audit Trails

```sql
-- Create audit table
CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    user_name TEXT,
    operation TEXT,
    table_name TEXT,
    old_values JSONB,
    new_values JSONB,
    client_ip INET
);

-- Create audit function
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_log (user_name, operation, table_name, old_values, new_values, client_ip)
    VALUES (
        current_user,
        TG_OP,
        TG_TABLE_NAME,
        CASE WHEN TG_OP != 'INSERT' THEN row_to_json(OLD) ELSE NULL END,
        CASE WHEN TG_OP != 'DELETE' THEN row_to_json(NEW) ELSE NULL END,
        inet_client_addr()
    );
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Attach to TVIEW tables
CREATE TRIGGER audit_tv_post
    AFTER INSERT OR UPDATE OR DELETE ON tv_post
    FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
```

### Monitor Suspicious Activity

```sql
-- Alert on unusual patterns
CREATE OR REPLACE FUNCTION monitor_suspicious_activity()
RETURNS VOID AS $$
DECLARE
    suspicious_count INTEGER;
BEGIN
    -- Check for excessive TVIEW creations
    SELECT COUNT(*) INTO suspicious_count
    FROM audit_log
    WHERE operation = 'CREATE_TVIEW'
      AND timestamp > NOW() - INTERVAL '1 hour'
      AND user_name != 'pg_tviews_admin';

    IF suspicious_count > 10 THEN
        -- Send alert
        RAISE WARNING 'Suspicious TVIEW creation activity detected';
    END IF;
END;
$$ LANGUAGE plpgsql;
```

## Performance Security

### Prevent Resource Exhaustion

```sql
-- Set reasonable limits
ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM SET maintenance_work_mem = '256MB';
ALTER SYSTEM SET max_parallel_workers_per_gather = 2;

-- Limit TVIEW complexity
CREATE OR REPLACE FUNCTION validate_tview_complexity()
RETURNS TRIGGER AS $$
DECLARE
    join_count INTEGER;
    table_count INTEGER;
BEGIN
    -- Count JOINs in the SQL
    SELECT
        array_length(regexp_split_array(NEW.sql_definition, 'JOIN|FROM'), 1) - 1,
        array_length(regexp_split_array(NEW.sql_definition, 'FROM'), 1)
    INTO join_count, table_count;

    IF join_count > 5 THEN
        RAISE EXCEPTION 'TVIEW too complex: % JOINs (max 5)', join_count;
    END IF;

    IF table_count > 3 THEN
        RAISE EXCEPTION 'TVIEW too complex: % tables (max 3)', table_count;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Attach to metadata table
CREATE TRIGGER validate_tview_complexity_trigger
    BEFORE INSERT OR UPDATE ON pg_tview_meta
    FOR EACH ROW EXECUTE FUNCTION validate_tview_complexity();
```

## Incident Response

### Security Breach Procedures

1. **Immediate Response**
   ```sql
   -- Disconnect suspicious sessions
   SELECT pg_terminate_backend(pid)
   FROM pg_stat_activity
   WHERE usename = 'suspicious_user';

   -- Disable public access
   REVOKE ALL ON ALL TABLES IN SCHEMA public FROM PUBLIC;
   ```

2. **Investigation**
   ```sql
   -- Check audit logs
   SELECT * FROM audit_log
   WHERE timestamp > NOW() - INTERVAL '24 hours'
   ORDER BY timestamp DESC;

   -- Check for unauthorized TVIEWs
   SELECT * FROM pg_tview_meta
   WHERE created_at > NOW() - INTERVAL '24 hours';
   ```

3. **Recovery**
   ```sql
   -- Restore from clean backup
   -- Recreate TVIEWs from trusted definitions
   -- Update security policies
   ```

## Best Practices Summary

1. **Validate all inputs** before using in SQL
2. **Use parameterized queries** instead of string concatenation
3. **Exclude sensitive data** from TVIEWs
4. **Implement RLS** for multi-tenant applications
5. **Grant minimal permissions** required
6. **Enable audit logging** for critical operations
7. **Monitor resource usage** to prevent DoS
8. **Regular security reviews** of TVIEW definitions
9. **Keep backups secure** and test restoration
10. **Have incident response plan** ready

## See Also

- [API Reference]../reference/api.md - Function permissions
- [Troubleshooting Guide]troubleshooting.md - Security-related issues
- [Monitoring Guide]monitoring.md - Production security monitoring