Skip to content

Commit 290ad68

Browse files
committed
Implement materialize views in PostgreSQL source support.
1 parent 0070036 commit 290ad68

File tree

10 files changed

+144
-30
lines changed

10 files changed

+144
-30
lines changed

pgloader.asd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
:serial t
181181
:depends-on ("common")
182182
:components ((:file "pgsql-cast-rules")
183+
(:file "pgsql-schema")
183184
(:file "pgsql")))))
184185

185186
;; package pgloader.copy

src/package.lisp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,9 @@
452452

453453
#:create-distributed-table
454454

455+
#:make-including-expr-from-catalog
456+
#:make-including-expr-from-view-names
457+
455458
;; finalizing catalogs support (redshift and other variants)
456459
#:finalize-catalogs
457460
#:adjust-data-types

src/parsers/command-materialize-views.lisp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,16 @@
66
;;;
77
(in-package #:pgloader.parser)
88

9-
(defrule view-name (and (alpha-char-p character)
10-
(* (or (alpha-char-p character)
11-
(digit-char-p character)
12-
#\_)))
13-
(:text t))
9+
(defrule view-name (or qualified-table-name maybe-quoted-namestring)
10+
(:identity t))
1411

1512
(defrule view-sql (and kw-as dollar-quoted)
1613
(:destructure (as sql) (declare (ignore as)) sql))
1714

1815
(defrule view-definition (and view-name (? view-sql))
1916
(:destructure (name sql) (cons name sql)))
2017

21-
(defrule another-view-definition (and comma view-definition)
18+
(defrule another-view-definition (and comma-separator view-definition)
2219
(:lambda (source)
2320
(bind (((_ view) source)) view)))
2421

src/parsers/command-pgsql.lisp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
alter-table alter-schema
111111
((:including incl))
112112
((:excluding excl))
113+
views
113114
distribute
114115
&allow-other-keys)
115116
`(lambda ()
@@ -129,6 +130,7 @@
129130
(copy-database source
130131
:including ',incl
131132
:excluding ',excl
133+
:materialize-views ',views
132134
:alter-table ',alter-table
133135
:alter-schema ',alter-schema
134136
:index-names :preserve
@@ -146,7 +148,7 @@
146148
pg-dst-db-uri
147149
&key
148150
gucs casts before after after-schema options
149-
alter-table alter-schema distribute
151+
alter-table alter-schema views distribute
150152
including excluding decoding)
151153
source
152154
(cond (*dry-run*
@@ -155,6 +157,7 @@
155157
(lisp-code-for-loading-from-pgsql pg-src-db-uri pg-dst-db-uri
156158
:gucs gucs
157159
:casts casts
160+
:views views
158161
:before before
159162
:after after
160163
:after-schema after-schema

src/parsers/command-utils.lisp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
(defrule ignore-whitespace (* whitespace)
3131
(:constant nil))
3232

33-
(defrule punct (or #\, #\- #\_ #\$ #\%)
33+
(defrule punct (or #\- #\_ #\$ #\%)
3434
(:text t))
3535

3636
(defrule namestring (and (or #\_ (alpha-char-p character))

src/pgsql/pgsql-schema.lisp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@
119119
(table-name table))
120120
:single)))
121121

122+
(defun make-including-expr-from-view-names (view-names)
123+
"Turn MATERIALIZING VIEWs list of view names into an INCLUDING parameter."
124+
(let (including current-schema)
125+
(loop :for (schema-name . view-name) :in view-names
126+
:do (let* ((schema-name
127+
(if schema-name
128+
(ensure-unquoted schema-name)
129+
(or
130+
current-schema
131+
(setf current-schema
132+
(pomo:query "select current_schema()" :single)))))
133+
(table-expr
134+
(make-string-match-rule :target (ensure-unquoted view-name)))
135+
(schema-entry
136+
(or (assoc schema-name including :test #'string=)
137+
(progn (push (cons schema-name nil) including)
138+
(assoc schema-name including :test #'string=)))))
139+
(push-to-end table-expr (cdr schema-entry))))
140+
;; return the including alist
141+
including))
142+
122143

123144
(defvar *table-type*
124145
'((:table . ("r" "f" "p")) ; ordinary, foreign and partitioned

src/sources/pgsql/pgsql-schema.lisp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
(in-package :pgloader.source.pgsql)
2+
3+
(defun create-pg-views (views-alist)
4+
"VIEWS-ALIST associates view names with their SQL definition, which might
5+
be empty for already existing views. Create only the views for which we
6+
have an SQL definition."
7+
(unless (eq :all views-alist)
8+
(let ((views (remove-if #'null views-alist :key #'cdr)))
9+
(when views
10+
(loop :for (name . def) :in views
11+
:for sql := (destructuring-bind (schema . v-name) name
12+
(format nil
13+
"CREATE VIEW ~s.~s AS ~a"
14+
schema v-name def))
15+
:do (progn
16+
(log-message :info "PostgreSQL Source: ~a" sql)
17+
#+pgloader-image
18+
(pgsql-execute sql)
19+
#-pgloader-image
20+
(restart-case
21+
(pgsql-execute sql)
22+
(use-existing-view ()
23+
:report "Use the already existing view and continue"
24+
nil)
25+
(replace-view ()
26+
:report
27+
"Replace the view with the one from pgloader's command"
28+
(let ((drop-sql (format nil "DROP VIEW ~a;" (car name))))
29+
(log-message :info "PostgreSQL Source: ~a" drop-sql)
30+
(pgsql-execute drop-sql)
31+
(pgsql-execute sql))))))))))
32+
33+
(defun drop-pg-views (views-alist)
34+
"See `create-pg-views' for VIEWS-ALIST description. This time we DROP the
35+
views to clean out after our work."
36+
(unless (eq :all views-alist)
37+
(let ((views (remove-if #'null views-alist :key #'cdr)))
38+
(when views
39+
(let ((sql
40+
(with-output-to-string (sql)
41+
(format sql "DROP VIEW ")
42+
(loop :for view-definition :in views
43+
:for i :from 0
44+
:do (destructuring-bind (name . def) view-definition
45+
(declare (ignore def))
46+
(format sql
47+
"~@[, ~]~s.~s"
48+
(not (zerop i)) (car name) (cdr name)))))))
49+
(log-message :info "PostgreSQL Source: ~a" sql)
50+
(pgsql-execute sql))))))

src/sources/pgsql/pgsql.lisp

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,37 +76,67 @@
7676
including
7777
excluding)
7878
"PostgreSQL introspection to prepare the migration."
79-
(declare (ignore materialize-views only-tables))
79+
(declare (ignore only-tables))
8080
(with-stats-collection ("fetch meta data"
8181
:use-result-as-rows t
8282
:use-result-as-read t
8383
:section :pre)
8484
(with-pgsql-transaction (:pgconn (source-db pgsql))
8585
(let ((variant (pgconn-variant (source-db pgsql)))
8686
(pgversion (pgconn-major-version (source-db pgsql))))
87-
(when (eq :pgdg variant)
88-
(list-all-sqltypes catalog
89-
:including including
90-
:excluding excluding))
87+
;;
88+
;; First, create the source views that we're going to materialize in
89+
;; the target database.
90+
;;
91+
(when (and materialize-views (not (eq :all materialize-views)))
92+
(create-pg-views materialize-views))
93+
94+
(when (eq :pgdg variant)
95+
(list-all-sqltypes catalog
96+
:including including
97+
:excluding excluding))
9198

92-
(list-all-columns catalog
93-
:including including
94-
:excluding excluding)
99+
(list-all-columns catalog
100+
:including including
101+
:excluding excluding)
95102

96-
(when create-indexes
97-
(list-all-indexes catalog
98-
:including including
99-
:excluding excluding
100-
:pgversion pgversion))
103+
(let* ((view-names (unless (eq :all materialize-views)
104+
(mapcar #'car materialize-views)))
105+
(including (make-including-expr-from-view-names view-names)))
106+
(cond (view-names
107+
(list-all-columns catalog
108+
:including including
109+
:table-type :view))
101110

102-
(when (and (eq :pgdg variant) foreign-keys)
103-
(list-all-fkeys catalog
104-
:including including
105-
:excluding excluding))
111+
((eq :all materialize-views)
112+
(list-all-columns catalog :table-type :view))))
113+
114+
(when create-indexes
115+
(list-all-indexes catalog
116+
:including including
117+
:excluding excluding
118+
:pgversion pgversion))
106119

107-
;; return how many objects we're going to deal with in total
108-
;; for stats collection
109-
(+ (count-tables catalog) (count-indexes catalog)))))
120+
(when (and (eq :pgdg variant) foreign-keys)
121+
(list-all-fkeys catalog
122+
:including including
123+
:excluding excluding))
124+
125+
;; return how many objects we're going to deal with in total
126+
;; for stats collection
127+
(+ (count-tables catalog)
128+
(count-views catalog)
129+
(count-indexes catalog)
130+
(count-fkeys catalog)))))
110131

111132
;; be sure to return the catalog itself
112133
catalog)
134+
135+
136+
(defmethod cleanup ((pgsql copy-pgsql) (catalog catalog) &key materialize-views)
137+
"When there is a PostgreSQL error at prepare-pgsql-database step, we might
138+
need to clean-up any view created in the source PostgreSQL connection for
139+
the migration purpose."
140+
(when materialize-views
141+
(with-pgsql-transaction (:pgconn (source-db pgsql))
142+
(drop-pg-views materialize-views))))

test/mysql/db789.load

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ LOAD DATABASE
44

55
WITH data only, truncate, create no tables
66

7-
MATERIALIZE VIEWS proceed
7+
MATERIALIZE VIEWS proceed, foo as $$ select 1 as a; $$
88

99
INCLUDING ONLY TABLE NAMES MATCHING 'proceed'
1010

@@ -13,5 +13,6 @@ LOAD DATABASE
1313
$$ drop schema if exists db789 cascade; $$,
1414
$$ create schema db789; $$,
1515
$$ create table db789.refrain (id char(1) primary key); $$,
16-
$$ create table db789.proceed (id char(1) primary key); $$;
16+
$$ create table db789.proceed (id char(1) primary key); $$,
17+
$$ create table db789.foo (a integer primary key); $$;
1718

test/pgsql-source.load

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,12 @@ load database
33
into pgsql://localhost/copy
44

55
-- including only table names matching 'bits', ~/utilisateur/ in schema 'mysql'
6+
including only table names matching ~/geolocations/ in schema 'public'
7+
8+
materialize views public.some_usps
9+
as $$
10+
select usps, geoid, aland, awater, aland_sqmi, awater_sqmi, location
11+
from districts
12+
where usps in ('MT', 'DE', 'AK', 'WY', 'PR', 'VT', 'SD', 'DC', 'ND');
13+
$$
614
;

0 commit comments

Comments
 (0)