Skip to content
This repository has been archived by the owner on Mar 4, 2019. It is now read-only.

Commit

Permalink
feat: support updatable views (fixes #528)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmfay committed May 28, 2018
1 parent 4cf7761 commit 660ac98
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 196 deletions.
59 changes: 27 additions & 32 deletions lib/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,39 +222,34 @@ Database.prototype.reload = function () {
}));
}

// attach the introspection scripts
this.loader.queryFiles = introspectors;

return this.$p.all(initPromises).then(() => {
// run introspections and flatten the results into a single list of Entities
return this.$p.all([{
ctor: Writable,
loaders: ['tables']
}, {
ctor: Readable,
loaders: ['views']
}, {
ctor: Executable,
loaders: ['functions', 'scripts']
}].reduce((promises, defn) => {
// attach the introspection scripts
this.loader.queryFiles = introspectors;

// for each ctor + associated loaders:
return promises.concat(defn.loaders.map(loader => {
// run the loader
const loaderFunc = require(`./loader/${loader}`); // eslint-disable-line global-require

return loaderFunc(this.instance, this.loader).then(loaded => {
// then afterwards map whatever it got into objects defined by ctor
return loaded.map(obj => {
obj = _.extend(obj, this.loader);
obj.db = this;
obj.path = !obj.schema || obj.schema === this.currentSchema ? obj.name : [obj.schema, obj.name].join('.');

return new (Function.prototype.bind.apply(defn.ctor, [null, obj]))();
});
});
}));
return this.$p.all(['tables', 'views', 'functions', 'scripts'].map(loader => {
// first find and execute the loader function
const loaderFunc = require(`./loader/${loader}`); // eslint-disable-line global-require

return loaderFunc(this.instance, this.loader).then(loaded => {
// then afterwards map whatever it got into objects defined by ctor
return loaded.map(obj => {
obj = _.extend(obj, this.loader);
obj.db = this;
obj.path = !obj.schema || obj.schema === this.currentSchema ? obj.name : [obj.schema, obj.name].join('.');
obj.loader = loader;

if (Object.hasOwnProperty.call(obj, 'is_insertable_into')) {
if (obj.is_insertable_into) {
return new (Function.prototype.bind.apply(Writable, [null, obj]))();
}

return new (Function.prototype.bind.apply(Readable, [null, obj]))();
}

// return promises.concat(entityLoaders);
return new (Function.prototype.bind.apply(Executable, [null, obj]))();
});
});
}, [])).then(groupedObjects => {
return _.flatten(groupedObjects);
}).then(objs => {
Expand All @@ -272,7 +267,7 @@ Database.prototype.reload = function () {
*/
Database.prototype.listTables = function () {
return this.objects.reduce((names, obj) => {
if (obj instanceof Writable) {
if (obj.loader === 'tables') {
names.push(obj.path);
}

Expand All @@ -287,7 +282,7 @@ Database.prototype.listTables = function () {
*/
Database.prototype.listViews = function () {
return this.objects.reduce((names, obj) => {
if (obj instanceof Readable && !(obj instanceof Writable)) {
if (obj.loader === 'views') {
names.push(obj.path);
}

Expand Down
2 changes: 2 additions & 0 deletions lib/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @param {String} spec.name - The entity's name.
* @param {String} spec.path - Path to the entity, if a file.
* @param {String} spec.schema - Entity's owning schema, if a database object.
* @param {String} spec.loader - Name of the loader that discovered the entity.
*/
const Entity = function (spec) {
if (spec.path) {
Expand All @@ -20,6 +21,7 @@ const Entity = function (spec) {
this.schema = spec.schema || spec.db.currentSchema;
this.name = spec.name;
this.db = spec.db;
this.loader = spec.loader;

// create delimited names now instead of at query time
this.delimitedName = `"${this.name}"`;
Expand Down
8 changes: 4 additions & 4 deletions lib/scripts/tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ SELECT * FROM (
t.table_name AS name,
parent.relname AS parent,
array_agg(DISTINCT kc.column_name::text) FILTER (WHERE kc.column_name IS NOT NULL) AS pk,
TRUE AS is_insertable_into,
array_agg(DISTINCT c.column_name::text) AS columns
array_agg(DISTINCT c.column_name::text) AS columns,
TRUE AS is_insertable_into
FROM information_schema.tables t
LEFT OUTER JOIN information_schema.table_constraints tc
ON tc.table_schema = t.table_schema
Expand Down Expand Up @@ -51,8 +51,8 @@ SELECT * FROM (
t.table_name AS name,
NULL AS parent,
NULL AS pk,
CASE t.is_insertable_into WHEN 'YES' THEN TRUE ELSE FALSE END AS is_insertable_into,
array_agg(c.column_name::text) AS columns
array_agg(c.column_name::text) AS columns,
CASE t.is_insertable_into WHEN 'YES' THEN TRUE ELSE FALSE END AS is_insertable_into
FROM information_schema.tables t
JOIN information_schema.columns c
ON c.table_schema = t.table_schema
Expand Down
18 changes: 16 additions & 2 deletions lib/scripts/views-legacy.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,23 @@
-- override blacklisted tables.

SELECT * FROM (
SELECT schemaname AS schema, viewname AS name, FALSE AS is_matview
FROM pg_views
SELECT
v.schemaname AS schema,
v.viewname AS name,
array_agg(DISTINCT c.column_name::text) AS columns,
pg_relation_is_updatable(cls.oid::regclass, true) & 8 >= 8 AS is_insertable_into,
FALSE AS is_matview
FROM pg_views v
JOIN pg_catalog.pg_namespace nsp
ON nsp.nspname = v.schemaname
JOIN pg_catalog.pg_class cls
ON cls.relnamespace = nsp.oid
AND cls.relname = v.viewname
JOIN information_schema.columns c
ON c.table_schema = v.schemaname
AND c.table_name = v.viewname
WHERE schemaname <> 'pg_catalog' AND schemaname <> 'information_schema'
GROUP BY v.schemaname, v.viewname, cls.oid
) views
WHERE CASE
WHEN $(whitelist) <> '' THEN
Expand Down
39 changes: 35 additions & 4 deletions lib/scripts/views.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,43 @@
-- override blacklisted tables.

SELECT * FROM (
SELECT schemaname AS schema, viewname AS name, FALSE AS is_matview
FROM pg_views
SELECT
v.schemaname AS schema,
v.viewname AS name,
array_agg(DISTINCT c.column_name::text) AS columns,
pg_relation_is_updatable(cls.oid::regclass, true) & 8 >= 8 AS is_insertable_into,
FALSE AS is_matview
FROM pg_views v
JOIN pg_catalog.pg_namespace nsp
ON nsp.nspname = v.schemaname
JOIN pg_catalog.pg_class cls
ON cls.relnamespace = nsp.oid
AND cls.relname = v.viewname
JOIN information_schema.columns c
ON c.table_schema = v.schemaname
AND c.table_name = v.viewname
WHERE schemaname <> 'pg_catalog' AND schemaname <> 'information_schema'
GROUP BY v.schemaname, v.viewname, cls.oid

UNION
SELECT schemaname AS schema, matviewname AS name, TRUE AS is_matview
FROM pg_matviews

SELECT
schemaname AS schema,
matviewname AS name,
array_agg(DISTINCT c.attname::text) AS columns,
FALSE AS is_insertable_into,
TRUE AS is_matview
FROM pg_matviews v
JOIN pg_catalog.pg_namespace nsp
ON nsp.nspname = v.schemaname
JOIN pg_catalog.pg_class cls
ON cls.relnamespace = nsp.oid
AND cls.relname = v.matviewname
JOIN pg_attribute c
ON c.attrelid = cls.oid
-- ordinary columns have positive attnum
AND c.attnum > 0
GROUP BY v.schemaname, v.matviewname
) views
WHERE CASE
WHEN $(whitelist) <> '' THEN
Expand Down

0 comments on commit 660ac98

Please sign in to comment.