diff --git a/api/src/org/labkey/api/data/DbSchema.java b/api/src/org/labkey/api/data/DbSchema.java index a4adc6d3ea6..e6fe9a8e865 100644 --- a/api/src/org/labkey/api/data/DbSchema.java +++ b/api/src/org/labkey/api/data/DbSchema.java @@ -612,15 +612,16 @@ public static class DDLMethodsTestCase extends Assert public void testDDLMethods() throws Exception { DbSchema testSchema = test.getSchema(); + DbScope testScope = testSchema.getScope(); // create test objects //start with cleanup - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop"); - testSchema.getSqlDialect().dropSchema(testSchema,"testdrop2"); - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop3"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop"); + testSchema.getSqlDialect().dropSchema(testScope,"testdrop2"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop3"); testSchema.dropTableIfExists(tempTableName); - SqlExecutor executor = new SqlExecutor(testSchema); + SqlExecutor executor = new SqlExecutor(testScope); executor.execute("CREATE SCHEMA testdrop"); executor.execute("CREATE SCHEMA testdrop2"); @@ -654,16 +655,16 @@ public void testDDLMethods() throws Exception testSchema.dropTableIfExists(tempTableName); executor.execute(sqlCreateTempTable); - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop"); // these don't exist testSchema.dropIndexIfExists("T", "T_notexist") ; testSchema.dropTableIfExists("V1"); testSchema.dropTableIfExists("Tnot"); - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop"); - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop2"); - testSchema.getSqlDialect().dropSchema(testSchema, "testdrop3"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop2"); + testSchema.getSqlDialect().dropSchema(testScope, "testdrop3"); } @After diff --git a/api/src/org/labkey/api/data/dialect/SqlDialect.java b/api/src/org/labkey/api/data/dialect/SqlDialect.java index 0db60369654..9a2688d87dd 100644 --- a/api/src/org/labkey/api/data/dialect/SqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/SqlDialect.java @@ -1588,11 +1588,13 @@ public String getDatabaseName(String url) throws ServletException /** * Drop a schema if it exists. * Throws an exception if schema exists and could not be dropped. + * @param scope DbScope where the schema might exist + * @param schemaName Name of the schema to drop. Casing must match the name in the database exactly. By convention, + * schema names are all lowercase, but quoting supports mixed case and uppercase as well. */ - public void dropSchema(DbSchema schema, String schemaName) + public void dropSchema(DbScope scope, String schemaName) { - SQLFragment sql = schema.getSqlDialect().execute(CoreSchema.getInstance().getSchema(), "fn_dropifexists", new SQLFragment("?, ?, ?, ?", "*", schemaName, "SCHEMA", null)); - new SqlExecutor(schema).execute(sql); + new SqlExecutor(scope).execute("DROP SCHEMA IF EXISTS " + quoteIdentifier(schemaName)+ " CASCADE"); } /** diff --git a/api/src/org/labkey/api/module/ModuleLoader.java b/api/src/org/labkey/api/module/ModuleLoader.java index 85e92ede24b..183b5771e6e 100644 --- a/api/src/org/labkey/api/module/ModuleLoader.java +++ b/api/src/org/labkey/api/module/ModuleLoader.java @@ -1885,11 +1885,11 @@ public void removeModule(ModuleContext context, boolean deleteFiles) Module m = getModule(moduleName); SchemaActions schemaActions = getSchemaActions(m, context); - schemaActions.deleteList().forEach(schema -> { - _log.info("Dropping schema \"{}\"", schema); - new SqlExecutor(_core.getSchema()).execute(sql, moduleName, schema + "-%"); - scope.getSqlDialect().dropSchema(_core.getSchema(), schema); - scope.invalidateSchema(schema, DbSchemaType.Unknown); // Invalidates all versions of the schema and tables in the non-provisioned caches (e.g., module, bare, fast) + schemaActions.deleteList().forEach(schemaName -> { + _log.info("Dropping schema \"{}\"", schemaName); + new SqlExecutor(_core.getSchema()).execute(sql, moduleName, schemaName + "-%"); + scope.getSqlDialect().dropSchema(scope, schemaName); + scope.invalidateSchema(schemaName, DbSchemaType.Unknown); // Invalidates all versions of the schema and tables in the non-provisioned caches (e.g., module, bare, fast) SchemaNameCache.get().remove(scope); // Invalidates the list of schema names associated with this scope }); diff --git a/core/module.properties b/core/module.properties index f6eddb9cc4b..8285cf71395 100644 --- a/core/module.properties +++ b/core/module.properties @@ -1,6 +1,6 @@ Name: Core ModuleClass: org.labkey.core.CoreModule -SchemaVersion: 26.007 +SchemaVersion: 26.008 Label: Administration and Essential Services Description: The Core module provides central services such as login, \ security, administration, folder management, user management, \ diff --git a/core/resources/schemas/dbscripts/postgresql/core-26.007-26.008.sql b/core/resources/schemas/dbscripts/postgresql/core-26.007-26.008.sql new file mode 100644 index 00000000000..b40e817ac37 --- /dev/null +++ b/core/resources/schemas/dbscripts/postgresql/core-26.007-26.008.sql @@ -0,0 +1,100 @@ +-- Remove the ability to drop AGGREGATE, DEFAULT, FUNCTION, PROCEDURE, and SCHEMA. These are not used in current +-- scripts and, if needed in the future, conditional drops are easy to affect in standard PostgreSQL DDL now. This +-- stored function is deprecated and all calls have been removed on develop. However, we're leaving the function in +-- place (supporting only five object types) for now to ease merges forward from older branches that might use it. +CREATE OR REPLACE FUNCTION core.fn_dropifexists( + text, + text, + text, + text) + RETURNS integer AS +$BODY$ +/* + Function to safely drop database object types without error if the object does not exist. + + Column deletion will cascade to any dependent objects. + + Usage: + SELECT core.fn_dropifexists(objname, objschema, objtype, subobjname), where: + + objname Required. For TABLE or VIEW, the name of the object to be dropped. + For INDEX, CONSTRAINT, or COLUMN, the name of the associated table. + objschema Required. The name of the object's schema + objtype Required. The type of object being dropped. Valid values are TABLE, VIEW, INDEX, CONSTRAINT, COLUMN. + subobjtype Required. When dropping INDEX, CONSTRAINT, or COLUMN, the name of the object being dropped. Otherwise, NULL. + */ +DECLARE + objname ALIAS FOR $1; + objschema ALIAS FOR $2; + objtype ALIAS FOR $3; + subobjname ALIAS FOR $4; + ret_code INTEGER; + fullname TEXT; + tempschema TEXT; +BEGIN + ret_code := 0; + fullname := (LOWER(objschema) || '.' || LOWER(objname)); + IF (UPPER(objtype)) = 'TABLE' THEN + BEGIN + IF EXISTS( SELECT * FROM pg_tables WHERE tablename = LOWER(objname) AND schemaname = LOWER(objschema) ) + THEN + EXECUTE 'DROP TABLE ' || fullname; + ret_code = 1; + ELSE + BEGIN + SELECT INTO tempschema schemaname FROM pg_tables WHERE tablename = LOWER(objname) AND schemaname LIKE '%temp%'; + IF (tempschema IS NOT NULL) + THEN + EXECUTE 'DROP TABLE ' || tempschema || '.' || objname; + ret_code = 1; + END IF; + END; + END IF; + END; + ELSEIF (UPPER(objtype)) = 'VIEW' THEN + BEGIN + IF EXISTS( SELECT * FROM pg_views WHERE viewname = LOWER(objname) AND schemaname = LOWER(objschema) ) + THEN + EXECUTE 'DROP VIEW ' || fullname; + ret_code = 1; + END IF; + END; + ELSEIF (UPPER(objtype)) = 'INDEX' THEN + BEGIN + fullname := LOWER(objschema) || '.' || LOWER(subobjname); + IF EXISTS( SELECT * FROM pg_indexes WHERE tablename = LOWER(objname) AND indexname = LOWER(subobjname) AND schemaname = LOWER(objschema) ) + THEN + EXECUTE 'DROP INDEX ' || fullname; + ret_code = 1; + ELSE + IF EXISTS( SELECT * FROM pg_indexes WHERE indexname = LOWER(subobjname) AND schemaname = LOWER(objschema) ) + THEN RAISE EXCEPTION 'INDEX - % defined on a different table.', subobjname; + END IF; + END IF; + END; + ELSEIF (UPPER(objtype)) = 'CONSTRAINT' THEN + BEGIN + IF EXISTS( SELECT * FROM pg_class LEFT JOIN pg_constraint ON conrelid = pg_class.oid INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace + WHERE relkind = 'r' AND contype IS NOT NULL AND nspname = LOWER(objschema) AND relname = LOWER(objname) AND conname = LOWER(subobjname) ) + THEN + EXECUTE 'ALTER TABLE ' || fullname || ' DROP CONSTRAINT ' || subobjname; + ret_code = 1; + END IF; + END; + ELSEIF (UPPER(objtype)) = 'COLUMN' THEN + BEGIN + IF EXISTS( SELECT * FROM information_schema.columns WHERE table_name = LOWER(objname) AND table_schema = LOWER(objschema) AND column_name = LOWER(subobjname)) + THEN + EXECUTE 'ALTER TABLE ' || fullname || ' DROP COLUMN ' || subobjname || ' CASCADE'; + ret_code = 1; + END IF; + END; + ELSE + RAISE EXCEPTION 'Invalid object type - %; Valid values are TABLE, VIEW, INDEX, CONSTRAINT, COLUMN ', objtype; + END IF; + + RETURN ret_code; +END; +$BODY$ +LANGUAGE plpgsql VOLATILE +COST 100; diff --git a/core/src/org/labkey/core/admin/sql/ScriptReorderer.java b/core/src/org/labkey/core/admin/sql/ScriptReorderer.java index e26851c2b84..f8a7c26c08f 100644 --- a/core/src/org/labkey/core/admin/sql/ScriptReorderer.java +++ b/core/src/org/labkey/core/admin/sql/ScriptReorderer.java @@ -93,8 +93,6 @@ public String getReorderedScript(boolean isHtml) patterns.add(new SqlPattern("ALTER TABLE " + TABLE_NAME_REGEX + " RENAME TO " + TABLE_NAME2_REGEX + STATEMENT_ENDING_REGEX, Type.Table, Operation.RenameTable)); patterns.add(new SqlPattern(getRegExWithPrefix("CREATE (TEMPORARY )?TABLE "), Type.Table, Operation.Other)); - patterns.add(new SqlPattern("SELECT core\\.fn_dropifexists\\s*\\('(?\\w+)'\\s*,\\s*'(?\\w+)'\\s*,\\s*'(TABLE|COLUMN|INDEX|DEFAULT|CONSTRAINT)'.+?" + STATEMENT_ENDING_REGEX, Type.Table, Operation.Other)); - patterns.add(new SqlPattern("SELECT core\\.fn_dropifexists\\s*\\('(\\w+)'\\s*,\\s*'(?\\w+)'.+?" + STATEMENT_ENDING_REGEX, Type.NonTable, Operation.Other)); patterns.add(new SqlPattern("SELECT SETVAL\\('" + TABLE_NAME_NO_UNDERSCORE_REGEX + "_.+?" + STATEMENT_ENDING_REGEX, Type.Table, Operation.Other)); patterns.add(new SqlPattern(getRegExWithPrefix("CLUSTER \\w+ ON "), Type.Table, Operation.Other)); // e.g. CLUSTER PK_Keyword ON flow.Keyword patterns.add(new SqlPattern(getRegExWithPrefix("CLUSTER "), Type.Table, Operation.Other));