pax_global_header00006660000000000000000000000064152271460530014517gustar00rootroot0000000000000052 comment=c8730243ca993de96122641452a138f284ef0437 darold-pgtt-fe8b2d5/000077500000000000000000000000001522714605300144415ustar00rootroot00000000000000darold-pgtt-fe8b2d5/.github/000077500000000000000000000000001522714605300160015ustar00rootroot00000000000000darold-pgtt-fe8b2d5/.github/workflows/000077500000000000000000000000001522714605300200365ustar00rootroot00000000000000darold-pgtt-fe8b2d5/.github/workflows/main.yml000066400000000000000000000036631522714605300215150ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the master branch push: branches: [ master ] pull_request: branches: [ master ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: runs-on: ubuntu-latest defaults: run: shell: bash strategy: matrix: pgversion: - 13 - 14 - 15 - 16 - 17 - 18 - 19 env: PGVERSION: ${{ matrix.pgversion }} steps: - name: checkout uses: actions/checkout@master - name: install pg run: | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v $PGVERSION -p -i sudo -u postgres createuser -s "$USER" - name: build run: | make PROFILE="-Werror" sudo -E make install - name: config run: | HBA=/etc/postgresql/$PGVERSION/main/pg_hba.conf sudo tee $HBA > /dev/null <<'EOF' local all all trust host all all 127.0.0.1/32 trust host all all ::1/128 trust local replication all trust host replication all 127.0.0.1/32 trust host replication all ::1/128 trust EOF sudo pg_conftool $PGVERSION main set session_preload_libraries pgtt sudo service postgresql restart - name: test run: | make installcheck - name: show regression diffs if: ${{ failure() }} run: | cat regression.diffs darold-pgtt-fe8b2d5/.gitignore000066400000000000000000000007321522714605300164330ustar00rootroot00000000000000# Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp *.bc # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe #*.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf # Test results results/ regression.* darold-pgtt-fe8b2d5/AUTHORS000066400000000000000000000001121522714605300155030ustar00rootroot00000000000000PGTT extension is written by: * Gilles Darold darold-pgtt-fe8b2d5/CONTRIBUTORS000066400000000000000000000001031522714605300163130ustar00rootroot00000000000000List of contributors ==================== Thanks to all of them! darold-pgtt-fe8b2d5/COPYING000066400000000000000000000013351522714605300154760ustar00rootroot00000000000000Copyright (c) 2018-2026 Gilles Darold Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. darold-pgtt-fe8b2d5/ChangeLog000066400000000000000000000431371522714605300162230ustar00rootroot00000000000000Version 4.6 - July 19 2026 This is a maintenance release ro fix several security and triggers or foreign keys related issues. - Copy on the temporary table the triggers defined on the "template" table of a global temporary table. CREATE TABLE ... (LIKE ...) has no INCLUDING TRIGGERS option in PostgreSQL, so the triggers must be replicated by hand once the temporary table has been created. The definition of each trigger is obtained through pg_get_triggerdef(), parsed back, and the relation of the resulting CreateTrigStmt is changed to point to the temporary table before being executed. - Fix duplicate key violation when trigger populates pgtt table. Thanks to Adrian Billington for the report. - Security hardening: lock down PUBLIC grants, fix possible SQL injection, close FK/DROP gaps. Thanks to Devrim Gunduz for the patch. * pg_global_temp_tables catalog table: no longer grants ALL to PUBLIC (every install script since 2.0.0 did). Any authenticated role could previously INSERT/UPDATE/DELETE/TRUNCATE another user's GTT registration directly, bypassing every ownership check the extension performs on CREATE/DROP/RENAME TABLE, and could enumerate every GTT's full DDL across the whole database. PUBLIC now gets SELECT only; roles that create/rename/drop GTTs need an additional explicit GRANT on this table (documented in the new install/upgrade scripts). * gtt_create_table_statement(): per-GTT "template" tables are now granted SELECT to PUBLIC instead of ALL, removing the ability for arbitrary roles to write to, truncate, or attach triggers to a table they do not own. * Fixed SQL injection (CWE-89): six internally generated SPI queries interpolated the user-supplied table name directly into single-quoted string literals without escaping, while adjacent code in the same functions correctly used quote_literal_cstr() for other values. A table name containing an embedded quote (e.g. a double-quoted identifier) could break out of the literal and inject additional SQL, executed with the caller's privileges -- or with a SECURITY DEFINER function's elevated privileges, if such a function builds a GTT name dynamically from caller input. All six call sites now consistently use quote_literal_cstr(). * Fixed missing-authorization check (CWE-862) in the DROP TABLE path: when a dropped table isn't in the current backend's local GTT cache, pgtt now checks pg_class ownership of the underlying relation before deleting its pg_global_temp_tables row, instead of deleting it unconditionally and relying on the *subsequent* DROP TABLE's own permission check (and transaction abort) to undo the side effect after the fact. * Replaced the regex-based foreign-key rejection with inspection of the parsed statement: the previous check searched the raw query text for the literal substring "FOREIGN KEY", which missed the equally valid column-level "col type REFERENCES ..." syntax entirely (it contains no such substring). The new gtt_tableelts_has_foreign_key() walks the parsed CreateStmt's columns/constraints directly, matching the approach already used by the ALTER TABLE handler. * Replaced five unchecked strcpy() calls into fixed NAMEDATALEN buffers (gtt.relname, pgtt_namespace_name) and one inside the GttHashTableInsert macro with bounds-checked strlcpy(). Not currently reachable as an overflow (PostgreSQL truncates identifiers before they reach this code), but removes reliance on that as the only safety net. - Build failure on PostgreSQL 16 and later: pg_class_ownercheck() was removed in PG16, which replaced the entire family of per-catalog pg_*_ownercheck() functions with a single generic object_ownercheck(classid, objectid, roleid) (both declared in utils/acl.h). Thanks to Devrim Gunduz for the patch. - Add security regression tests. Thanks to Devrim Gunduz for the patch. * test/sql/15_security_grants.sql -- proves PUBLIC now has SELECT only (not ALL) on pgtt_schema.pg_global_temp_tables and on a GTT's template table: an unprivileged role can still read both, but every INSERT/UPDATE/DELETE/TRUNCATE against them is rejected. Also proves a role provisioned per the documented non-superuser setup (schema CREATE + the new required table-level grant) can still fully create, use, and drop a GTT end to end. * test/sql/16_sql_injection.sql -- creates, renames, and uses a GTT whose name is a double-quoted identifier containing an embedded single quote and a complete extra SQL statement. * test/sql/17_drop_authorization.sql -- has a non-owner role whose backend cache predates a GTT's creation attempt to DROP TABLE it; the drop must be rejected and the registration must survive intact. This specifically exercises the "not cached" lookup path in the DROP TABLE handler, where the missing ownership check lived. * test/sql/10_foreignkey.sql -- adds the column-level "c1 integer REFERENCES other(id)" form alongside the existing table-level "FOREIGN KEY (c1) REFERENCES other(id)" cases, both directly in CREATE and added later via ALTER TABLE, and confirms it is still allowed on an ordinary (non-GTT) table. Version 4.5 - June 18 2026 This is a maintenance release to add support to PG v19 and fix issues reported by users since last release. - Add support to PG v19. - Fix -Wmissing-variable-declarations warnings by making globals static. Thanks to Japin Li for the patch. - Fix CI workflow. - Add support to CREATE INDEX CONCURRENTLY ... Thanks to Haibo Yan for the patch. - Remove use of the registered relid of the relation. When restoring pgtt_schema.pg_global_temp_tables from a dump it contains invalid Oid for the relation. Thanks to sibowu-aws for the report. Version 4.4 - July 02 2025 This is a maintenance release to fix a search_path error when using pg_dump. - Fix pg_dump error with invalid search_path. pg_dump set search_path to empty string, appending the pgtt schema in this case make it fail. - Update regression test expected output file related to search_path test. Version 4.3 - June 09 2025 This is a quick maintenance release to fix an issue introduced in latest release. - Drop extension before running tests on search_path. - Fix invalid search_path error. When pgtt isn't preloaded and pg_catalog is the last element in search_path, results in error message "invalid value for parameter 'search_path'" due to invalid list syntax. Thanks to Japin Li for the patch. Version 4.2 - June 08 2025 This is a maintenance release to fix issues reported by users since latest release and especially a search_path modification issue. - Preserve pg_catalog at end of search path if it is the case to allow pg_catalog overloaded function to be executed. - Add regression test on search_path forcing. - Preserve $user and other elements in search_path. Thanks to Japin Li for the patch. - Remove the LOAD command from documentation. Thanks to Li Jianping for the patch. - Fix session_preload_libraries documentation for non-superusers. Thanks to Japin Li for the patch. - Resolve a warning regarding incompatible pointer types. Thanks to Li Jianping for the patch. Version 4.1 - March 18 2025 This is a maintenance release to fix issues reported by users since latest release. - Use query instead of \d to list indexes in regression tests. - Fix wrong complain about temporary table created and active when a regular temporary table is created with (LIKE ... INCLUDING INDEXES). Thanks to Benjamin Wirth for the report. - Remove documentation for non-superuser configuration. Thanks to Julien Rouhaud for the patch. - Fix config in README. The concrete path to the shared library is not needed in postgresql.conf. Thanks to Simon Martin for the patch. - Install docs to a non-generic name, add a symlink to README.md that is referenced in DOCS. Thanks to Christoph Berg for the patch. - Add vaccum pg_class + pg_sleep(1) after each drop table in regression tests to wait that the temporary table is really removed. - Add missing PGDLLEXPORT tags on callback functions, those are needed on windows. Thanks to Julien Rouhaud for the patch. Version 4.0 - May 31 2024 This major release allow preloading pgtt in session_preload_libraries and adds support to Windows operating system. Here is the complete list of changes. - Add port to Windows operating system. Note that, for some unknown reasons it doesn't work at all with PostgreSQL 13. Thanks to Julien Rouhaud for the patch - Allow preloading pgtt in session_preload_libraries. The module is now lazily loaded when it might be needed (after parse analysis, before executor startup and before utility statements execution), and all the code is simply bypassed if the underlying extension has not been created. Thanks to Julien Rouhaud for the patch. - Fix first query execution after a LOAD command. If the extension is loaded with a plain LOAD command, the search_path will only be set during the next query execution. It means that the very first query executed after such a LOAD wouldn't see the global temporary tables. Thanks to Julien Rouhaud for the patch. - Remove the relocation test, the extension is not relocatable anymore. - Fix crash with RESET ALL. RESET ALL has a NULL "name" field, so we can't use strcmp in that case. Since the existing code is only interested in SET commands, just move up the check that the command is a VAR_SET_VALUE to ensure that we will have a GUC name. Thanks to Julien Rouhaud for the patch. - Fix various minor whitespace and indent issues. Thanks to Julien Rouhaud for the patch. - Add Julen Rouhaud in the authors and maintainers list. Version 3.2 - Apr 12 2024 This is a maintenance release to fix issues reported by users since latest release and compatibility with future PG 17. - Add information about privilege on the pgtt schema. - Make test on lock compatible with version <= 14 - The temporary table needs to be locked. Thanks to Leo X.M. Zeng for the patch. - Replace MyBackendId with MyProcNumber for PG > 16 - Check extension when loading pgtt. Thanks to Japin Li for the patch. - Release lock on global template temporary table. Thanks to Japin Li for the patch. Version 3.1 - Dec 26 2023 This is a maintenance release to fix issues reported by users since latest release. - Fix ERROR: attempt to redefine parameter "pgtt.enabled". Thanks to Japin Li for the report. - Forget acquiring lock for temporary table. Thanks to Japin Li for the patch. - Add temporisation after vacuum in 08_plplgsql test. - Update main.yml to remove PG versions prior 12. Version 3.0 - Sep 17 2023 This major release fix several issues related to PostgreSQL v16 port and remove support to PostgreSQL version prior 12. Here is the complete list of changes. - Remove support to PostgreSQL prior v12. - Fix compilation error: static declaration of get_extension_schema follows non-static declaration. Thanks to Devrim Gunduz for the report. - Remove grouping set in regexp. - Add -Wno-ignored-attributes to CPPFLAGS to avoid compilation warning on pg_vsnprintf call. - Replace GetOverrideSearchPath() call by GetSearchPathMatcher() with PG > 16 - Fix debug information print on Windows. Thanks to Lanlan for the patch. - Replace reg* regexp function with the pg_reg* function. Thanks to Lanlan for the report. - Fix port to PostgreSQL v16. Thanks to Julien Rouhaud for the patch. Version 2.10 - Feb 23 2023 This is a maintenance release to fix a lock issue in multi parallel process environment, many locks were generated. Here is the complete list of changes. - Delegate locks on per session temporary tables to PostgreSQL when the table will be used. - Release lock on parent table after temporary table creation based on this relation. Thanks to jayhsiang and liyaojinli for the report. - Use vacuum in test 08 to not look at pg_class too early before the temporary table is remove. - Fix setting default value of the parent table's persistence that can be misleading in future development. Thanks to songjinzhou for the patch. - Fix documentation. Thanks to Luca Ferrari for the patch. Version 2.9 - Aug 16 2022 This is a maintenance release per CVE-2022-2625 and new PostgreSQL minor versions updates. - Remove creation of the pgtt_schema if it not exists from extension files per CVE-2022-2625. Thanks to Dmitry Ukolov for the report. - Add regression test for regular table drop. Version 2.8 - Jun 02 2022 This is a maintenance release to add support to PostgreSQL 15 and fix an error when trying to drop a regular table. - Add support to PostgreSQL 15. - Fix impossibility to drop a regular table when the extension is loaded. Thanks to basildba for the report. Version 2.7 - Nov 23 2021 This is a maintenance release to fix an issue with parallelism and improve performances. - Prevent code to be executed in parallel processes. Thanks to Dmitry Ukolov for the report. - Improve performances by not looking for an existing GTT table if the table is a temporary table or part of the pg_catalog. - Update ChangeLog to acknowledge patch on PG14 support to Dmitry Ukolov. - Update copyright year. Version 2.6 - Sep 22 2021 This is a maintenance release to add support for upcomming PostgreSQL 14 and fixed some issues reported in the past three months. - Add support to PostgreSQL 14. Thanks to Devrim Gunduz for the report and Dmitry Ukolov for the patch. - Remove support to PG 9.5 which obviously was not working. Minimal PG version for this extension is 9.6. - Fix documentation menu. - Fix creation of GTT when there is a CHECK constraint with string constant. Version 2.5 - Jun 08 2021 This is a maintenance release to hotfix port on PostgreSQL 9.6. - Fix port to PostgreSQL 9.6. Thanks to Devrim Gunduz for the report. Version 2.4 - Jun 04 2021 This version allow use of the extension by non superuser and especially the creation and maintenance of GTT. It also fixes compatibility with PostgreSQL v14. Here is the full list of changes: - Fix FailedAssertion "flags & HASH_STRINGS" with PG14. Thanks to MigOps for the patch. - Check for minimum pg version in the C code instead of Makefile. Thanks to MigOps for the patch. - Fixed compiling for PostgreSQL 14. Thanks to Dmitry Ukolov for the patch. - Fix documentation about privilege to set on pgtt_schema for a non superuser role. - Allow creation and maintenance of Global Temporary Tables by non superuser. This require that the user can use schema pgtt_schema and can write to table pg_schema.pg_global_temp_tables. - The library can now be loaded by the user using: LOAD '$libdir/plugins/pgtt.so'; Thanks to Dmitry Ukolov for the feature request. - Fix two crashes when --enable-cassert is used. Thanks to hanson69 for the report. - Fix comment and index on PGTT table. Thanks to Dmitry Ukolov for the report. - Fix unexpected error "attempt to create referential integrity constraint on global temporary table" when creating a regular table and fix detection of FK and throw an error on create global temporary table statement. Thanks to Dmitry Ukolov for the report. - Fix impossibility to recreate GTT if it was dropped in another session. Thanks to Dmitry Ukolov for the report. - Remove useless extension's downgrade files. Thanks to MigOps for the patch. Version 2.3 - Apr 02 2021 This version fix the compatibility with PostgreSQL 10 and 11. When use on PostgreSQL 10 and 11 the following was raised ERROR: unrecognized node type: 375 Thanks to smallcookie086 for the report. Add upgrade/downgrade SQL files. Version 2.2 - Nov 08 2020 This release is a port of the extension for PostgreSQL v12 and v13. Works now on all PostgreSQL version from v9.5 to current. It also fixes automatic creation of the underlying temporary table after a rollback. Other fixes: - Fix regression test for all supported PG version. - Replace call to \d in regression tests, they do not report the same information following PG version. - Remove test about partitioning as it returns a failure for PG < 10. - Update regression tests to avoid failure related to temp table id. - Add regression test for error on rollback issue. Version 2.1 - May 11 2020 This is a maintenance release to complete the work on the extension and fix some issues. * Prevent use of foreign keys with GTT, not that PostgreSQL do not allow it but just to mimic the behavior of Oracle and other RDBMS like DB2, SQL Server and MySQL for example. * Raise an error on an attempt to partition a Global Temporary Table. This is not supported, again not because PostgreSQL do not allow partition on temporary table but because other RDBMS like Oracle, DB2 and MySQL do not support it. * Add support to comments, constraints and identity columns clauses when creating the GTT. Other fixes: - Add regression tests on partitioning and FK. - Exclude regression.* files from git scope. - Improve documentation and add information about constraints. - Add documentation about unsupported FK and partition on GTT. - Fix missing files for expected test results. - Fix exclusion of .out and results directory. - Update regression tests about changes on CREATE TABLE ... LIKE. - Fix some typo in documentation and markdown titles. Version 2.0 - April 19 2020 Initial release. darold-pgtt-fe8b2d5/INSTALL000066400000000000000000000000671522714605300154750ustar00rootroot00000000000000The installation steps are covered in the README file. darold-pgtt-fe8b2d5/META.json000066400000000000000000000020331522714605300160600ustar00rootroot00000000000000{ "name": "pgtt", "abstract": "Extension to add Global Temporary Tables feature to PostgreSQL.", "version": "4.6.0", "maintainer": "Gilles Darold ", "license": "postgresql", "release_status": "stable", "provides": { "pgtt": { "abstract": "Extension to manage Global Temporary Tables", "file": "sql/pgtt--4.6.0.sql", "docfile": "doc/pgtt.md", "version": "4.6.0" } }, "resources": { "bugtracker": { "web": "https://github.com/darold/pgtt/issues" }, "repository": { "url": "git://github.com/darold/pgtt.git", "web": "https://github.com/darold/pgtt.git", "type": "git" } }, "prereqs": { "runtime": { "requires": { "PostgreSQL": "12.0" } } }, "generated_by": "Gilles Darold", "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" }, "tags": [ "global", "temporary", "table" ] } darold-pgtt-fe8b2d5/Makefile000066400000000000000000000020071522714605300161000ustar00rootroot00000000000000EXTENSION = pgtt EXTVERSION = $(shell grep default_version $(EXTENSION).control | \ sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") PGFILEDESC = "pgtt - Global Temporary Tables for PostgreSQL" PG_CONFIG = pg_config PG_CPPFLAGS = -I$(libpq_srcdir) -Wno-uninitialized -Wno-ignored-attributes PG_LDFLAGS = -L$(libpq_builddir) -lpq PG_LIBDIR := $(shell $(PG_CONFIG) --libdir) SHLIB_LINK = $(libpq) DOCS = pgtt.md MODULES = pgtt DATA = $(wildcard updates/*--*.sql) $(wildcard sql/*.sql) TESTS = 00_init 01_oncommitdelete 02_oncommitpreserve \ 03_createontruncate 04_rename 05_useindex \ 06_createas 07_createlike 08_plplgsql \ 09_transaction 10_foreignkey 11_after_error \ 12_droptable 13_searchpath 14_concurrent_index \ 15_security_grants 16_sql_injection 17_drop_authorization \ 18_subquery 19_trigger REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS_OPTS = --inputdir=test PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) darold-pgtt-fe8b2d5/README.md000066400000000000000000000442431522714605300157270ustar00rootroot00000000000000* [Description](#description) * [Installation](#installation) * [Configuration](#configuration) * [Use of the extension](#use-of-the-extension) * [How the extension really works](#how-the-extension-really-works) * [Performances](performances) * [Authors](#authors) ## PostgreSQL Global Temporary Tables ### [Description](#description) pgtt is a PostgreSQL extension to create, manage and use Oracle-style Global Temporary Tables and the others RDBMS. The objective of this extension it to provide the Global Temporary Table feature to PostgreSQL waiting for an in core implementation. The main interest of this extension is to mimic the Oracle behavior with GTT when you can not or don't want to rewrite the application code when migrating to PostgreSQL. In all other case best is to rewrite the code to use standard PostgreSQL temporary tables. This version of the GTT extension use a regular unlogged table as "template" table and an internal rerouting to a temporary table. See chapter "How the extension really works" for more details. A previous implementation of this extension using Row Security Level is still available [here](https://github.com/darold/pgtt-rsl). PostgreSQL native temporary tables are automatically dropped at the end of a session, or optionally at the end of the current transaction. Global Temporary Tables (GTT) are permanent, they are created as regular tables visible to all users but their content is relative to the current session or transaction. Even if the table is persistent a session or transaction can not see rows written by an other session. Usually this is not a problem, you have learn to deal with the temporary table behavior of PostgreSQL but the problem comes when you are migrating an Oracle database to PostgreSQL. You have to rewrite the SQL and PlPgSQL code to follow the application logic and use PostgreSQL temporary table, that mean recreating the temporary table everywhere it is used. The other advantage of this kind of object is when your application creates and drops a lot of temporary tables, the PostgreSQL catalogs becomes bloated and the performances start to fall. Usually Global Temporary Tables prevent catalog bloating, but with this implementation and even if we have a permanent table, all DML are rerouted to a regular temporary table created at first access. See below chapter "How the extension really works" for more information. DECLARE TEMPORARY TABLE statement is not supported by PostgreSQL and by this extension. However this statement defines a temporary table for the current connection / session, it creates tables that do not reside in the system catalogs and are not persistent. It cannot be shared with other sessions. This is the equivalent of PostgreSQL standard CREATE TEMPORARY TABLE so you might just have to replace the DECLARE keyword by CREATE. All Oracle's GTT behavior are respected with the different clauses minus what is not supported by PostgreSQL: #### ON COMMIT {DELETE | PRESERVE} ROWS Specifies the action taken on the global temporary table when a COMMIT operation is performed. - DELETE ROWS: all rows of the table will be deleted if no holdable cursor is open on the table. - PRESERVE ROWS: the rows of the table will be preserved after the COMMIT. #### LOGGED or NOT LOGGED [ ON ROLLBACK {DELETE | PRESERVE} ROWS ] Specifies whether operations for the table are logged. The default is `NOT LOGGED ON ROLLBACK DELETE ROWS`. * NOT LOGGED: Specifies that insert, update, or delete operations against the table are not to be logged, but that the creation or dropping of the table is to be logged. During a ROLLBACK or ROLLBACK TO SAVEPOINT operation: - If the table had been created within a transaction, the table is dropped - If the table had been dropped within a transaction, the table is recreated, but without any data * ON ROLLBACK: Specifies the action that is to be taken on the not logged created temporary table when a ROLLBACK or ROLLBACK TO SAVEPOINT operation is performed. The default is DELETE ROWS. - DELETE ROWS: if the table data has been changed, all the rows will be deleted. - PRESERVE ROWS: rows of the table will be preserved. * LOGGED: specifies that insert, update, or delete operations against the table as well as the creation or dropping of the table are to be logged. With PostgreSQL only `NOT LOGGED ON ROLLBACK DELETE ROWS` can be supported. Creation or dropping of the Global Temporary Table are logged, see below "How the extension really works" for the details. ### [Installation](#installation) To install the pgtt extension you need at least a PostgreSQL version 12. Untar the pgtt tarball anywhere you want then you'll need to compile it with pgxs. The `pg_config` tool must be in your path. Depending on your installation, you may need to install some devel package. Once `pg_config` is in your path, do make sudo make install Then it will be possible to use it using `session_preload_libraries = 'pgtt'` in postgresql.conf To create and manage GTT using a non-superuser role you will have to grant the CREATE privilege on the `pgtt_schema` schema to the user. For example: GRANT ALL ON SCHEMA pgtt_schema TO pgtt_user1; To run test execute the following command as superuser: make installcheck An additional standalone test is provided to test the use of the extension as non superuser. The test can be executed using: mkdir results createdb gtt_privilege LANG=C psql -d gtt_privilege -f test/privilege.sql > results/privilege.out 2>&1 diff results/privilege.out test/expected/privilege.out dropdb gtt_privilege dropuser pgtt_user1 ### [Configuration](#configuration) - *pgtt.enabled* The extension can be enable / disable using this GUC, default is enabled. To disable the extension use: SET pgtt.enabled TO off; You can disable or enable the extension at any moment in a session. ### [Use of the extension](#use-of-the-extension) In all database where you want to use Global Temporary Tables you will have to create the extension using: CREATE EXTENSION pgtt; You can load the extension by setting in postgresql.conf : session_preload_libraries = 'pgtt'; or by setting it at database level as follow: DO $$ BEGIN EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''pgtt''', current_database()); END $$; The pgtt extension use a dedicated schema to store related objects, by default: `pgtt_schema`. The extension take care that this schema is always at end of the `search_path`. gtt_testdb=# SHOW search_path; search_path -------------------- public,pgtt_schema (1 row) gtt_testdb=# SET search_path TO appschema,public; SET gtt_testdb=# SHOW search_path; search_path -------------------------------- appschema, public, pgtt_schema (1 row) The pgtt schema is automatically added to the search_path when you load the extension and if you change the `search_path` later. You must also give the USAGE privilege on this schema to users that will manipulate the global temporary tables. #### Create a Global Temporary Table To create a GTT table named "test_table" use the following statement: CREATE GLOBAL TEMPORARY TABLE test_gtt_table ( id integer, lbl text ) ON COMMIT { PRESERVE | DELETE } ROWS; The GLOBAL keyword is obsolete but can be used safely, the only thing is that it will generate a warning: WARNING: GLOBAL is deprecated in temporary table creation If you don't want to be annoyed by this warning message you can use it like a comment instead: CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table ( LIKE other_table LIKE INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES ) ON COMMIT { PRESERVE | DELETE } ROWS; the extension will detect the GLOBAL keyword. As you can see in the example above the LIKE clause is supported, as well as the AS clause WITH DATA or WITH NO DATA (default): CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table AS SELECT * FROM source_table WITH DATA; In case of WITH DATA, the extension will fill the GTT with data returned from the SELECT statement for the current session only. PostgreSQL temporary table clause `ON COMMIT DROP` is not supported by the extension, GTT are persistent over transactions. If the clause is used an error will be raised. Temporary table rows are deleted or preserved at transactions commit following the clause: ON COMMIT { PRESERVE | DELETE } ROWS #### Drop a Global Temporary Table To drop a Global Temporary Table you just proceed as for a normal table: DROP TABLE test_gtt_table; A Global Temporary Table can be dropped even if it is used by other session. #### Create index on Global Temporary Table You can create indexes on the global temporary table: CREATE INDEX ON test_gtt_table (id); just like with any other tables. #### Constraints on Global Temporary Table You can add any constraint on a Global Temporary Table except FOREIGN KEYS. CREATE GLOBAL TEMPORARY TABLE t2 ( c1 serial PRIMARY KEY, c2 VARCHAR (50) UNIQUE NOT NULL, c3 boolean DEFAULT false ) The use of FOREIGN KEYS in a Global Temporary Table is not allowed. CREATE GLOBAL TEMPORARY TABLE t1 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); ERROR: attempt to create referential integrity constraint on global temporary table ALTER TABLE t2 ADD FOREIGN KEY (c1) REFERENCES source (id); ERROR: attempt to create referential integrity constraint on global temporary table Even if PostgreSQL allow foreign keys on temporary table, the pgtt extension try to mimic as much as possible the same behavior of Oracle and other RDBMS like DB2, SQL Server or MySQL. ORA-14455: attempt to create referential integrity constraint on temporary table. #### Partitioning Partitioning on Global Temporary Table is not supported, again not because PostgreSQL do not allow partition on temporary table but because other RDBMS like Oracle, DB2 and MySQL do not support it. SQL Server supports partition on global temporary table. #### Triggers Triggers defined on or using a Global Temporary Table are supported since version 4.6. ### [How the extension really works](#how-the-extension-really-works) #### Global Temporary Table usage When `pgtt.enabled` is true (default) and the extension have been loaded using any of the three methods: - `session_preload_libraries = 'pgtt'` in postgresql.conf - `ALTER DATABASE mydb SET session_preload_libraries = 'pgtt'` - in a session `LOAD 'pgtt';` the first access to the table using a SELECT, UPDATE or DELETE statement will produce the creation of a temporary table using the definition of the "template" table created during the call to `CREATE GLOBAL TEMPORARY TABLE` statement. Once the temporary table is created at the first access, the original SELECT, UPDATE or DELETE statement is automatically rerouted to the new regular temporary table. All other access will use the new temporary table, the `pg_temp*` schema where the table is created is always looked first in the search path this is why the "template" table is not concerned by subsequent access. Creating, renaming and removing a GTT is an administration task it shall not be done in an application session. Note that rerouting is active even if you add a namespace qualifier to the table. For example looking at the internal unlogged template table: bench=# LOAD 'pgtt'; LOAD bench=# CREATE /*GLOBAL*/ TEMPORARY TABLE test_tt (id int, lbl text) ON COMMIT PRESERVE ROWS; CREATE TABLE bench=# INSERT INTO test_tt VALUES (1, 'one'), (2, 'two'), (3, 'three'); INSERT 0 3 bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) will actually result in the same as looking at the associated temporary table like follow: bench=# SELECT * FROM test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) or bench=# SELECT * FROM pg_temp.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) If you want to really look at the template table to be sure that it contains no rows, you must disable the extension rerouting: bench=# SET pgtt.enabled TO off; SET bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+----- (0 rows) bench=# SET pgtt.enabled TO on; SET bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) Look at test file for more examples. This also mean that you can relocate the extension in a dedicated namespace. This can be useful if your application's queries use the schema qualifier with the table name to access to the GTT and you can't change it. See t/sql/relocation.sql for an example. By default the extension is not relocatable in an other schema, there is some configuration change to perform to be able to use this feature. If you use the CREATE AS form with the WITH DATA clause like in this example: CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table AS SELECT * FROM source_table WITH DATA; the extension will first create the template unlogged table and will create immediately the associated temporary table filled with all data returned by the SELECT statement. The first access will not have to create the table it already exists with data. #### Table creation The extension intercept the call to `CREATE TEMPORARY TABLE ...` statement and look if there is the keyword `GLOBAL` or the comment `/*GLOBAL*/`. When it is found, instead of creating the temporary table, it creates a "template" unlogged persistent table following the temporary table definition. When the template is created it registers the table into a "catalog" table `pg_global_temp_tables`. Both objects are created in the extension schema `pgtt_schema`. When `pgtt.enabled` is false nothing is done. Here is the description of the catalog table: ``` Table « pgtt_schema.pg_global_temp_tables » Colonne | Type | Collationnement | NULL-able | Par défaut -----------+---------+-----------------+-----------+------------ relid | integer | | not null | nspname | name | | not null | relname | name | | not null | preserved | boolean | | | code | text | | | Index : "pg_global_temp_tables_nspname_relname_key" UNIQUE CONSTRAINT, btree (nspname, relname) ``` * `relid`: Oid of the "template" unlogged table. * `nspname`: namespace of the extension `pgtt_schema` by default. * `relname`: name of the GTT relation. * `preserved`: true or false for `ON COMMIT { PRESERVE | DELETE}`. * `code`: code used at Global Temporary Table creation time. #### Table removing The extension intercept the call to `DROP TABLE` and look in the `pg_global_temp_tables` table to see if it is declared. When it is found it drops the template unlogged table and the corresponding entry from the pgtt catalog table `pg_global_temp_tables`. When `pgtt.enabled` is false nothing is done. Dropping a GTT that is in use, when the temporary table has already been created, will raise an error. This is not allowed. #### Table renaming The extension intercept the call to `ALTER TABLE ... RENAME` and look in the `pg_global_temp_tables` table to see if it is declared. When it is found it renames the "template" table and update the name of the relation in the `pg_global_temp_tables` table. If the GTT has already been used in the session the corresponding temporary table exists, in this case the extension will refuse to rename it. It must be inactive to be renamed. When `pgtt.enabled` is false nothing is done. Renaming a GTT that is in use, when the temporary table has already been created, will raise an error. This is not allowed. #### pg_dump / pg_restore When dumping a database using the pgtt extension, the content of the "catalog" table `pg_global_temp_tables` will be dumped as well as all template unlogged tables. Restoring the dump will recreate the database in the same state. ### [Performances](#performances) Overhead of loading the extension but without using it in a pgbench tpcb-like scenario. * Without loading the extension ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_noload.sql starting vacuum...end. transaction type: test/bench/bench_noload.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 51741 latency average = 23.201 ms tps = 862.038042 (including connections establishing) tps = 862.165341 (excluding connections establishing) ``` * With loading the extension ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_load.sql starting vacuum...end. transaction type: test/bench/bench_load.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 51171 latency average = 23.461 ms tps = 852.495877 (including connections establishing) tps = 852.599010 (excluding connections establishing) ``` Now a test between using a regular temporary table and a PGTT in the pgbench tpcb-like scenario. * Using a regular Temporary Table ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_use_rtt.sql starting vacuum...end. transaction type: test/bench/bench_use_rtt.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 17153 latency average = 70.058 ms tps = 285.477860 (including connections establishing) tps = 285.514186 (excluding connections establishing) ``` * Using a Global Temporary Table Created using: CREATE GLOBAL TEMPORARY TABLE test_tt (id int, lbl text) ON COMMIT DELETE ROWS; ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_use_gtt.sql starting vacuum...end. transaction type: test/bench/bench_use_gtt.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 17540 latency average = 68.495 ms tps = 291.993502 (including connections establishing) tps = 292.028832 (excluding connections establishing) ``` Even if this last test shows a significant performances improvement comparing to regular temporary tables, most of the time this will not be the case. ### [Authors](#authors) - Gilles Darold - Julien Rouhaud ### [License](#license) This extension is free software distributed under the PostgreSQL Licence. Copyright (c) 2018-2026, Gilles Darold darold-pgtt-fe8b2d5/pgtt.c000066400000000000000000002265701522714605300155770ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pgtt.c * Add support to Oracle-style Global Temporary Table in PostgreSQL. * * Author: Gilles Darold * Licence: PostgreSQL * Copyright (c) 2018-2026, Gilles Darold, * *------------------------------------------------------------------------- */ #include "postgres.h" #include #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" #include "access/htup_details.h" #include "access/parallel.h" #include "access/reloptions.h" #include "access/sysattr.h" #include "access/xact.h" #include "catalog/catalog.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" #include "catalog/pg_collation.h" #include "catalog/pg_database.h" #include "catalog/pg_extension.h" #include "catalog/pg_namespace.h" #include "catalog/pg_operator.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #include "catalog/toasting.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/extension.h" #include "commands/tablecmds.h" #include "commands/comment.h" #include "executor/spi.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "nodes/nodes.h" #include "nodes/pg_list.h" #include "nodes/print.h" #include "nodes/value.h" #include "optimizer/paths.h" #include "optimizer/plancat.h" #include "parser/analyze.h" #include "parser/parse_utilcmd.h" #include "parser/parser.h" #include "storage/ipc.h" #include "storage/lmgr.h" #include "storage/proc.h" #include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/formatting.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #if PG_VERSION_NUM < 110000 #include "utils/memutils.h" #endif /* for regexp search */ #include "regex/regexport.h" #if (PG_VERSION_NUM >= 120000) #include "access/genam.h" #include "access/heapam.h" #include "catalog/pg_class.h" #endif #if PG_VERSION_NUM < 120000 #error Minimum version of PostgreSQL required is 12 #endif #define CATALOG_GLOBAL_TEMP_REL "pg_global_temp_tables" #define Anum_pgtt_relid 1 #define Anum_pgtt_nspname 2 #define Anum_pgtt_relname 3 PG_MODULE_MAGIC; #define NOT_IN_PARALLEL_WORKER (ParallelWorkerNumber < 0) #if PG_VERSION_NUM >= 140000 #define STMT_OBJTYPE(stmt) stmt->objtype #else #define STMT_OBJTYPE(stmt) stmt->relkind #endif /* Define ProcessUtility hook proto/parameters following the PostgreSQL version */ #if PG_VERSION_NUM >= 140000 #define GTT_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ bool readOnlyTree, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ QueryCompletion *qc #define GTT_PROCESSUTILITY_ARGS pstmt, queryString, readOnlyTree, context, params, queryEnv, dest, qc #else #if PG_VERSION_NUM >= 130000 #define GTT_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ QueryCompletion *qc #define GTT_PROCESSUTILITY_ARGS pstmt, queryString, context, params, queryEnv, dest, qc #else #if PG_VERSION_NUM >= 100000 #define GTT_PROCESSUTILITY_PROTO PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, DestReceiver *dest, \ char *completionTag #define GTT_PROCESSUTILITY_ARGS pstmt, queryString, context, params, queryEnv, dest, completionTag #elif PG_VERSION_NUM >= 90300 #define GTT_PROCESSUTILITY_PROTO Node *parsetree, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ DestReceiver *dest, char *completionTag #define GTT_PROCESSUTILITY_ARGS parsetree, queryString, context, params, dest, completionTag #else #define GTT_PROCESSUTILITY_PROTO Node *parsetree, const char *queryString, \ ParamListInfo params, bool isTopLevel, \ DestReceiver *dest, char *completionTag #define GTT_PROCESSUTILITY_ARGS parsetree, queryString, params, isTopLevel, dest, completionTag #endif #endif #endif /* Saved hook values in case of unload */ static ProcessUtility_hook_type prev_ProcessUtility = NULL; static ExecutorStart_hook_type prev_ExecutorStart = NULL; static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL; /* Hook to intercept CREATE GLOBAL TEMPORARY TABLE query */ static void gtt_ProcessUtility(GTT_PROCESSUTILITY_PROTO); static void gtt_ExecutorStart(QueryDesc *queryDesc, int eflags); #if PG_VERSION_NUM >= 190000 static void gtt_post_parse_analyze(ParseState *pstate, Query *query, const JumbleState *jstate); #else #if PG_VERSION_NUM >= 140000 static void gtt_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate); #else static void gtt_post_parse_analyze(ParseState *pstate, Query *query); #endif #endif static void gtt_try_load(void); #if PG_VERSION_NUM < 160000 Oid get_extension_schema(Oid ext_oid); #endif static bool is_declared_gtt(Oid relid); /* Enable use of Global Temporary Table at session level */ static bool pgtt_is_enabled = true; /* Regular expression search */ #define CREATE_GLOBAL_REGEXP "^\\s*CREATE\\s+(?:\\/\\*\\s*)?GLOBAL(?:\\s*\\*\\/)?" /* Oid and name of pgtt extrension schema in the database */ static Oid pgtt_namespace_oid = InvalidOid; static char pgtt_namespace_name[NAMEDATALEN]; /* In memory storage of GTT and state */ typedef struct Gtt { Oid relid; Oid temp_relid; char relname[NAMEDATALEN]; bool preserved; bool created; char *code; } Gtt; typedef struct relhashent { char name[NAMEDATALEN]; Gtt gtt; } GttHashEnt; static HTAB *GttHashTable = NULL; /* Default size of the storage area for GTT but will be dynamically extended */ #define GTT_PER_DATABASE 16 #define GttHashTableDelete(NAME) \ do { \ GttHashEnt *hentry; \ \ hentry = (GttHashEnt *) hash_search(GttHashTable, NAME, HASH_REMOVE, NULL); \ if (hentry == NULL) \ elog(DEBUG1, "trying to delete GTT entry in HTAB that does not exist"); \ } while(0) #define GttHashTableLookup(NAME, GTT) \ do { \ GttHashEnt *hentry; \ \ hentry = (GttHashEnt *) hash_search(GttHashTable, \ (NAME), HASH_FIND, NULL); \ if (hentry) \ GTT = hentry->gtt; \ } while(0) #define GttHashTableInsert(GTT, NAME) \ do { \ GttHashEnt *hentry; bool found; \ \ hentry = (GttHashEnt *) hash_search(GttHashTable, \ (NAME), HASH_ENTER, &found); \ if (found) \ elog(ERROR, "duplicate GTT name"); \ hentry->gtt = GTT; \ strlcpy(hentry->name, NAME, sizeof(hentry->name)); \ elog(DEBUG1, "Insert GTT entry in HTAB, key: %s, relid: %d, temp_relid: %d, created: %d", hentry->gtt.relname, hentry->gtt.relid, hentry->gtt.temp_relid, hentry->gtt.created); \ } while(0) /* Function declarations */ PGDLLEXPORT void _PG_init(void); PGDLLEXPORT void _PG_fini(void); int strpos(char *hay, char *needle, int offset); static Oid gtt_create_table_statement(Gtt gtt); static void gtt_create_table_as(Gtt gtt, bool skipdata); static void gtt_unregister_global_temporary_table(const char *relname); void GttHashTableDeleteAll(void); bool EnableGttManager(void); Gtt GetGttByName(const char *name); static void gtt_load_global_temporary_tables(void); static Oid create_temporary_table_internal(ParseState *pstate, Oid parent_relid, bool preserved); static bool gtt_check_command(GTT_PROCESSUTILITY_PROTO); static bool gtt_table_exists(QueryDesc *queryDesc); void exitHook(int code, Datum arg); static bool is_catalog_relid(Oid relid); static void gtt_rewrite_rte(ParseState *pstate, Query *query, RangeTblEntry *rte); static void gtt_rewrite_query(ParseState *pstate, Query *query); static bool gtt_query_walker(Node *node, void *context); static void gtt_exec_utility_subcommand(Node *stmt, const char *querystring); static void gtt_copy_triggers(Oid parent_relid, const char *relname); static void gtt_set_trigger_enabled(const char *relname, const char *tgname, char tgenabled); static void force_pgtt_namespace (void); static void gtt_update_registered_table(Gtt gtt); int strremovestr(char *src, char *toremove); static void gtt_unregister_gtt_not_cached(const char *relname); static bool gtt_tableelts_has_foreign_key(List *tableElts); static bool gtt_current_user_can_drop(Oid relid); /* * Module load callback */ void _PG_init(void) { elog(DEBUG1, "_PG_init()"); if (ParallelWorkerNumber >= 0) return; /* * If we are loaded via shared_preload_libraries exit. */ if (process_shared_preload_libraries_in_progress) { ereport(FATAL, (errmsg("The pgtt extension can not be loaded using shared_preload_libraries."), errhint("Add 'pgtt' to session_preload_libraries globally, or" " for the wanted roles or databases instead."))); } /* * Define (or redefine) custom GUC variables. * No custom GUC variable at this time */ DefineCustomBoolVariable("pgtt.enabled", "Enable use of Global Temporary Table", "By default the extension is automatically enabled after load, " "it can be temporary disable by setting the GUC value to false " "then enable again later wnen necessary.", &pgtt_is_enabled, true, PGC_USERSET, 0, NULL, NULL, NULL); /* * Immediately try to load the extension. * * The GTT "template" tables are resolved through the search_path, which * is only fixed by force_pgtt_namespace() once the extension has been * loaded. This must therefore be done before the very first command of * the session goes through parse analysis, otherwise that command fails * with "relation ... does not exist" when it references a GTT. * * With PostgreSQL 15 and above, process_session_preload_libraries() is * called from InitPostgres(), inside the transaction started for the * backend initialization, so gtt_try_load() can do its catalog lookups * right away. With PostgreSQL 14 and below the session preloaded * libraries are loaded later, from PostgresMain(), after that transaction * has been committed: gtt_try_load() would be a no-op there because * IsTransactionState() returns false. Start a transaction ourselves in * this case. The same applies when the library is loaded through a plain * LOAD command outside of any transaction. */ if (IsTransactionState()) gtt_try_load(); else if (IsUnderPostmaster && OidIsValid(MyDatabaseId)) { StartTransactionCommand(); gtt_try_load(); CommitTransactionCommand(); } /* * Install hooks. */ prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = gtt_ExecutorStart; prev_post_parse_analyze_hook = post_parse_analyze_hook; post_parse_analyze_hook = gtt_post_parse_analyze; prev_ProcessUtility = ProcessUtility_hook; ProcessUtility_hook = gtt_ProcessUtility; /* set the exit hook */ on_proc_exit(&exitHook, PointerGetDatum(NULL)); } /* * Module unload callback */ void _PG_fini(void) { elog(DEBUG1, "_PG_fini()"); /* Uninstall hooks. */ ExecutorStart_hook = prev_ExecutorStart; post_parse_analyze_hook = prev_post_parse_analyze_hook; ProcessUtility_hook = prev_ProcessUtility; } /* * Exit hook. */ void exitHook(int code, Datum arg) { elog(DEBUG1, "exiting with %d", code); } static void gtt_ProcessUtility(GTT_PROCESSUTILITY_PROTO) { elog(DEBUG1, "gtt_ProcessUtility()"); /* Do not waste time here if the feature is not enabled for this session */ if (pgtt_is_enabled && NOT_IN_PARALLEL_WORKER) { /* Try to load pgtt if not already done. */ gtt_try_load(); /* * Be sure that extension schema is at end of the search path so that * "template" tables will be find. */ force_pgtt_namespace(); /* * Check if we have a CREATE GLOBAL TEMPORARY TABLE * in this case do more work than the simple table * creation see SQL file in sql/ subdirectory. * * If the current query use a GTT that is not already * created create it. */ if (gtt_check_command(GTT_PROCESSUTILITY_ARGS)) { elog(DEBUG1, "Work on GTT from Utility Hook done, get out of UtilityHook immediately."); return; } } elog(DEBUG1, "restore ProcessUtility"); /* Excecute the utility command, we are not concerned */ PG_TRY(); { if (prev_ProcessUtility) prev_ProcessUtility(GTT_PROCESSUTILITY_ARGS); else standard_ProcessUtility(GTT_PROCESSUTILITY_ARGS); } PG_CATCH(); { PG_RE_THROW(); } PG_END_TRY(); elog(DEBUG1, "End of gtt_ProcessUtility()"); } /* * Walk a CREATE TABLE statement's column/constraint list looking for a * foreign key constraint, in either the table-level * "FOREIGN KEY (...) REFERENCES ..." form (a bare Constraint node in * tableElts) or the column-level "col type REFERENCES ..." form (a * Constraint node attached to a ColumnDef's own constraint list). * Replaces a previous text-regex search for the literal substring * "FOREIGN KEY", which only caught the table-level form. */ static bool gtt_tableelts_has_foreign_key(List *tableElts) { ListCell *lc; foreach(lc, tableElts) { Node *elt = (Node *) lfirst(lc); if (IsA(elt, Constraint)) { Constraint *constr = (Constraint *) elt; if (constr->contype == CONSTR_FOREIGN) return true; } else if (IsA(elt, ColumnDef)) { ColumnDef *coldef = (ColumnDef *) elt; ListCell *lc2; foreach(lc2, coldef->constraints) { Constraint *constr = (Constraint *) lfirst(lc2); if (IsA(constr, Constraint) && constr->contype == CONSTR_FOREIGN) return true; } } } return false; } /* * Return true if the current user is allowed to DROP the given * relation (its owner, a role that inherits ownership rights, or a * superuser). Used before pgtt mutates the shared pg_global_temp_tables * catalog on behalf of a DROP TABLE statement that targets a relation * this backend hasn't cached locally, so that an unprivileged role * cannot deregister another user's Global Temporary Table simply by * naming it in a DROP TABLE statement that will itself go on to fail * PostgreSQL's own ownership check. */ static bool gtt_current_user_can_drop(Oid relid) { if (!OidIsValid(relid)) return false; /* * PostgreSQL 16 replaced the whole family of per-catalog * pg_*_ownercheck() functions (including pg_class_ownercheck()) * with a single generic object_ownercheck(classid, objectid, * roleid). Both are declared in utils/acl.h. */ #if PG_VERSION_NUM >= 160000 return object_ownercheck(RelationRelationId, relid, GetUserId()); #else return pg_class_ownercheck(relid, GetUserId()); #endif } /* * Look at utility command to search CREATE TABLE / DROP TABLE * and INSERT INTO statements to see if a Global Temporary Table * is concerned. * Return true if all work is done and the origin statement must * be forgotten. False mean that the statement must be processed * normally. */ static bool gtt_check_command(GTT_PROCESSUTILITY_PROTO) { bool preserved = true; bool work_completed = false; char *name = NULL; #if PG_VERSION_NUM >= 100000 Node *parsetree = pstmt->utilityStmt; #endif Assert(parsetree != NULL); Assert(queryString != NULL); elog(DEBUG1, "gtt_check_command() on query: \"%s\"", queryString); if (GttHashTable == NULL) return false; /* Intercept CREATE / DROP TABLE statements */ switch (nodeTag(parsetree)) { case T_VariableSetStmt: { VariableSetStmt *stmt = (VariableSetStmt *) parsetree; /* * Forcing search_path is not enough because it does not * handle SET search_path TO ... statement. This code also * add the PGTT schema if not present in the path */ if (stmt->kind == VAR_SET_VALUE && strcmp(stmt->name, "search_path") == 0) { ListCell *l; bool found = false; if (stmt->args == NIL) break; foreach(l, stmt->args) { Node *arg = (Node *) lfirst(l); A_Const *con = (A_Const *) arg; char *val; val = strVal(&con->val); if (strcmp(val, get_namespace_name(pgtt_namespace_oid)) == 0) found = true; } /* append the extension schema to the arg list. */ if (!found) { A_Const *newcon = makeNode(A_Const); char *str = (char *) get_namespace_name(pgtt_namespace_oid); #if PG_VERSION_NUM < 150000 newcon->val.type = T_String; newcon->val.val.str = pstrdup(str); #else newcon->val.node.type = T_String; newcon->val.sval.sval = pstrdup(str); #endif newcon->location = strlen(queryString); stmt->args = lappend(stmt->args, newcon); } } } break; case T_CreateTableAsStmt: { Gtt gtt; int i; CreateTableAsStmt *stmt = (CreateTableAsStmt *)parsetree; bool skipdata = stmt->into->skipData; bool regexec_result; /* Get the name of the relation */ name = stmt->into->rel->relname; /* * CREATE TABLE AS is similar as SELECT INTO, * so avoid going further in this last case. */ if (stmt->is_select_into) break; /* do not proceed OBJECT_MATVIEW */ if (STMT_OBJTYPE(stmt) != OBJECT_TABLE) break; /* * Be sure to have CREATE TEMPORARY TABLE definition */ if (stmt->into->rel->relpersistence != RELPERSISTENCE_TEMP) break; /* * We only take care here of statements with the GLOBAL keyword * even if it is deprecated and generate a warning. */ regexec_result = RE_compile_and_execute( cstring_to_text(CREATE_GLOBAL_REGEXP), VARDATA_ANY(cstring_to_text((char *) queryString)), VARSIZE_ANY_EXHDR(cstring_to_text((char *) queryString)), REG_ADVANCED | REG_ICASE | REG_NEWLINE, DEFAULT_COLLATION_OID, 0, NULL); if (!regexec_result) break; /* * What to do at commit time for global temporary relations * default is ON COMMIT PRESERVE ROWS (do nothing) */ if (stmt->into->onCommit == ONCOMMIT_DELETE_ROWS) preserved = false; /* * Case of ON COMMIT DROP and GLOBAL TEMPORARY might not be * allowed, this is the same as using a normal temporary table * inside a transaction. Here the table should be dropped after * commit so it will not survive a transaction. * Throw an error to prevent the use of this clause. */ if (stmt->into->onCommit == ONCOMMIT_DROP) ereport(ERROR, (errmsg("use of ON COMMIT DROP with GLOBAL TEMPORARY is not allowed"), errhint("Create a local temporary table inside a transaction instead, this is the default behavior."))); elog(DEBUG1, "Create table %s, rows persistance: %d, GLOBAL at position: %d", name, preserved, strpos(asc_toupper(queryString, strlen(queryString)), "GLOBAL", 0)); /* Force creation of the temporary table in our pgtt schema */ stmt->into->rel->schemaname = pstrdup(pgtt_namespace_name); /* replace temporary state from the table to unlogged table */ stmt->into->rel->relpersistence = RELPERSISTENCE_UNLOGGED; /* Do not copy data in the unlogged table */ stmt->into->skipData = true; /* * At this stage the unlogged table will be created with normal * utility hook. What we need now is to register the table in * the pgtt catalog table and create a normal temporary table * using the original statement without the GLOBAL keyword */ gtt.relid = 0; gtt.temp_relid = 0; strlcpy(gtt.relname, name, sizeof(gtt.relname)); gtt.preserved = preserved; gtt.created = false; /* Extract the AS ... code part from the query */ gtt.code = pstrdup(queryString); for (i = 30; i < strlen(queryString) - 1; i++) { if ( isspace(queryString[i]) && (queryString[i+1] == 'A' || queryString[i+1] == 'a') && (queryString[i+2] == 'S' || queryString[i+2] == 's') && (isspace(queryString[i+3]) || queryString[i+3] == '(') ) break; } if (i == strlen(queryString) - 1) elog(ERROR, "can not find AS keyword in this CREATE TABLE AS statement."); gtt.code += i; if (gtt.code[strlen(gtt.code) - 1] == ';') gtt.code[strlen(gtt.code) - 1] = 0; /* remove WITH DATA from the code */ strremovestr(gtt.code, "WITH DATA"); /* Create the necessary object to emulate the GTT */ gtt_create_table_as(gtt, skipdata); work_completed = true; break; } case T_CreateStmt: { /* CREATE TABLE statement */ CreateStmt *stmt = (CreateStmt *)parsetree; Gtt gtt; int len, i, start = 0, end = 0; bool regexec_result; /* Get the name of the relation */ name = stmt->relation->relname; /* * Be sure to have CREATE TEMPORARY TABLE definition */ if (stmt->relation->relpersistence != RELPERSISTENCE_TEMP) break; /* * We only take care here of statements with the GLOBAL keyword * even if it is deprecated and generate a warning. */ regexec_result = RE_compile_and_execute( cstring_to_text(CREATE_GLOBAL_REGEXP), VARDATA_ANY(cstring_to_text((char *) queryString)), VARSIZE_ANY_EXHDR(cstring_to_text((char *) queryString)), REG_ADVANCED | REG_ICASE | REG_NEWLINE, DEFAULT_COLLATION_OID, 0, NULL); if (!regexec_result) break; /* * Check if there is a foreign key defined in the statement. * A text-regex search for the literal substring "FOREIGN KEY" * only catches the table-level "FOREIGN KEY (...) REFERENCES * ..." form; it misses the equally valid column-level * "col type REFERENCES ..." form, which contains no such * substring at all and would silently slip through. Inspect * the parsed column/constraint list instead so both forms * are caught, matching how the ALTER TABLE handler below * already checks parsed Constraint nodes rather than text. */ if (gtt_tableelts_has_foreign_key(stmt->tableElts)) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("attempt to create referential integrity constraint on global temporary table"))); #if (PG_VERSION_NUM >= 100000) /* * We do not allow partitioning on GTT, not that PostgreSQL can * not do it but because we want to mimic the Oracle or other * RDBMS behavior. */ if (stmt->partspec != NULL) elog(ERROR, "Global Temporary Table do not support partitioning."); #endif /* * What to do at commit time for global temporary relations * default is ON COMMIT PRESERVE ROWS (do nothing) */ if (stmt->oncommit == ONCOMMIT_DELETE_ROWS) preserved = false; /* * Case of ON COMMIT DROP and GLOBAL TEMPORARY might not be * allowed, this is the same as using a normal temporary table * inside a transaction. Here the table should be dropped after * commit so it will not survive a transaction. * Throw an error to prevent the use of this clause. */ if (stmt->oncommit == ONCOMMIT_DROP) ereport(ERROR, (errmsg("use of ON COMMIT DROP with GLOBAL TEMPORARY is not allowed"), errhint("Create a local temporary table inside a transaction instead, this is the default behavior."))); elog(DEBUG1, "Create table %s, rows persistance: %d, GLOBAL at position: %d", name, preserved, strpos(asc_toupper(queryString, strlen(queryString)), "GLOBAL", 0)); /* Create the Global Temporary Table template and register the table */ gtt.relid = 0; gtt.temp_relid = 0; strlcpy(gtt.relname, name, sizeof(gtt.relname)); gtt.preserved = preserved; gtt.created = false; gtt.code = NULL; /* Extract the definition of the table */ for (i = 0; i < strlen(queryString); i++) { if (queryString[i] == '(') { start = i; break; } } start++; for (i = start; i < strlen(queryString); i++) { if (queryString[i] == ')') { end = i; } } len = end - start; if (end > 0 && start > 0) { gtt.code = palloc0(sizeof(char *) * (len + 1)); strncpy(gtt.code, queryString+start, len); gtt.code[len] = '\0'; } elog(DEBUG1, "code for Global Temporary Table \"%s\" creation is \"%s\"", gtt.relname, gtt.code); /* Create the necessary object to emulate the GTT */ gtt.relid = gtt_create_table_statement(gtt); /* * In case of problem during GTT creation previous function * call throw an error so the code that's follow is safe. * Update GTT cache with table flagged as created */ gtt.created = false; GttHashTableDelete(gtt.relname); GttHashTableInsert(gtt, gtt.relname); work_completed = true; elog(DEBUG1, "Global Temporary Table \"%s\" created", gtt.relname); break; } case T_DropStmt: { DropStmt *drop = (DropStmt *) parsetree; if (drop->removeType == OBJECT_TABLE) { List *relationNameList = NULL; int relationNameListLength = 0; #if PG_VERSION_NUM < 150000 Value *relationSchemaNameValue = NULL; Value *relationNameValue = NULL; #else String *relationSchemaNameValue = NULL; String *relationNameValue = NULL; #endif Gtt gtt; relationNameList = list_copy((List *) linitial(drop->objects)); relationNameListLength = list_length(relationNameList); switch (relationNameListLength) { case 1: { relationNameValue = linitial(relationNameList); break; } case 2: { relationSchemaNameValue = linitial(relationNameList); relationNameValue = lsecond(relationNameList); break; } case 3: { relationSchemaNameValue = lsecond(relationNameList); relationNameValue = lthird(relationNameList); break; } default: { ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("improper relation name: \"%s\"", NameListToString(relationNameList)))); break; } } /* prefix with schema name if it is not added already */ if (relationSchemaNameValue == NULL) { #if PG_VERSION_NUM < 150000 Value *schemaNameValue = makeString(pgtt_namespace_name); #else String *schemaNameValue = makeString(pgtt_namespace_name); #endif relationNameList = lcons(schemaNameValue, relationNameList); } /* * Check if the table is in the hash list, drop * it if it has already been be created and remove * the cache entry. */ #if PG_VERSION_NUM < 150000 if (PointerIsValid(relationNameValue->val.str)) #else if ((const void*)relationNameValue->sval != NULL) #endif { #if PG_VERSION_NUM < 150000 elog(DEBUG1, "looking for dropping table: %s", relationNameValue->val.str); #else elog(DEBUG1, "looking for dropping table: %s", relationNameValue->sval); #endif /* Initialize Gtt object */ gtt.relid = 0; gtt.temp_relid = 0; gtt.relname[0] = '\0'; gtt.preserved = false; gtt.code = NULL; gtt.created = false; #if PG_VERSION_NUM < 150000 elog(DEBUG1, "looking if table %s is a cached GTT", relationNameValue->val.str); GttHashTableLookup(relationNameValue->val.str, gtt); #else elog(DEBUG1, "looking if table %s is a cached GTT", relationNameValue->sval); GttHashTableLookup(relationNameValue->sval, gtt); #endif if (gtt.relname[0] != '\0') { /* * When the temporary table have been created * we can not remove the GTT in the same session. * Creating and dropping GTT can only be performed * by a superuser in a "maintenance" session. */ if (gtt.created) elog(ERROR, "can not drop a GTT that is in use."); /* * Unregister the Global Temporary Table and its link to the * view stored in pg_global_temp_tables table */ gtt_unregister_global_temporary_table(gtt.relname); /* Remove the table from the hash table */ GttHashTableDelete(gtt.relname); } else { /* * Table is not on current session cache but remove * it from PGTT list if it exists. */ #if PG_VERSION_NUM < 150000 elog(DEBUG1, "looking if table %s is registered as GTT", relationNameValue->val.str); gtt_unregister_gtt_not_cached(relationNameValue->val.str); #else elog(DEBUG1, "looking if table %s is registered as GTT", relationNameValue->sval); gtt_unregister_gtt_not_cached(relationNameValue->sval); #endif } } } break; } case T_RenameStmt: { /* CREATE TABLE statement */ RenameStmt *stmt = (RenameStmt *)parsetree; Gtt gtt; /* We only take care of tabe renaming to update our internal storage */ if (stmt->renameType != OBJECT_TABLE || stmt->newname == NULL) break; gtt.relid = 0; /* Look if the table is declared as GTT */ GttHashTableLookup(stmt->relation->relname, gtt); /* Not registered as a GTT, nothing to do here */ if (gtt.relid == 0) break; /* If a temporary table have already created do not allow changing name */ if (gtt.created) elog(ERROR, "a temporary table has been created and is active, can not rename the GTT table in this session."); /* Rename the table and get the resulting new Oid */ RenameRelation(stmt); elog(DEBUG1, "updating registered table in %s.pg_global_temp_tables.", pgtt_namespace_name); strlcpy(gtt.relname, stmt->newname, sizeof(gtt.relname)); gtt_update_registered_table(gtt); /* Delete and recreate the table in cache */ GttHashTableDelete(stmt->relation->relname); GttHashTableInsert(gtt, stmt->newname); work_completed = true; break; } case T_CommentStmt: { /* COMMENT ON TABLE/COLUMN statement */ CommentStmt *stmt = (CommentStmt *)parsetree; Relation relation; char *nspname; /* We only take care of comment on table or column to update our internal storage */ if (stmt->objtype != OBJECT_TABLE && stmt->objtype != OBJECT_COLUMN) break; /* * Get the relation object by calling get_object_address(). * get_object_address() will throw an error if the object * does not exist, and will also acquire a lock on the target * to guard against concurrent DROP operations. */ #if (PG_VERSION_NUM < 100000) (void) get_object_address(stmt->objtype, stmt->objname, stmt->objargs, &relation, ShareUpdateExclusiveLock, false); #else (void) get_object_address(stmt->objtype, stmt->object, &relation, ShareUpdateExclusiveLock, false); #endif /* Just take care that the GTT is not in use */ nspname = get_namespace_name(RelationGetNamespace(relation)); relation_close(relation, NoLock); if (strcmp(nspname, pgtt_namespace_name) != 0) { if (strstr(nspname, "pg_temp") != NULL) elog(ERROR, "a temporary table has been created and is active, can not add a comment on the GTT table in this session."); } break; } case T_AlterTableStmt: { /* Look for contrainst statement */ AlterTableStmt *stmt = (AlterTableStmt *)parsetree; ListCell *lcmd; Gtt gtt; if (STMT_OBJTYPE(stmt) != OBJECT_TABLE) break; /* Look if the table is declared as GTT */ gtt.relid = 0; GttHashTableLookup(stmt->relation->relname, gtt); /* Not registered as a GTT, nothing to do here */ if (gtt.relid == 0) break; /* We do not allow foreign keys on global temporary table */ foreach(lcmd, stmt->cmds) { AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lcmd); if (cmd->subtype == AT_AddConstraint #if (PG_VERSION_NUM < 130000) || cmd->subtype == AT_ProcessedConstraint #endif ) { Constraint *constr = (Constraint *) cmd->def; if (constr->contype == CONSTR_FOREIGN) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("attempt to create referential integrity constraint on global temporary table"))); } } break; } case T_IndexStmt: { /* CREATE INDEX statement */ IndexStmt *stmt = (IndexStmt *) parsetree; Oid relid; Oid nspid; char *nspname; LOCKMODE lockmode = ShareLock; /* * PATCH: Support CREATE INDEX CONCURRENTLY on active GTT instances. * We use ShareUpdateExclusiveLock to avoid blocking reads/writes * on the active session's data. */ if (stmt->concurrent) lockmode = ShareUpdateExclusiveLock; relid = RangeVarGetRelidExtended(stmt->relation, lockmode, #if (PG_VERSION_NUM >= 110000) 0, #else false, false, #endif RangeVarCallbackOwnsRelation, NULL); /* * Ensure that the global temporary table (GTT) is not currently in use. * A GTT is considered "in use" only when its corresponding local temporary * table has been created in the session. */ nspid = get_rel_namespace(relid); nspname = get_namespace_name(nspid); if (is_declared_gtt(relid)) { if (strcmp(nspname, pgtt_namespace_name) != 0) { if (isTempNamespace(nspid)) { if (!stmt->concurrent) ereport(ERROR, (errmsg("a temporary table has been created and is active, can not add an index on the GTT table in this session."))); } } } break; } default: break; } return work_completed; } static void gtt_ExecutorStart(QueryDesc *queryDesc, int eflags) { elog(DEBUG1, "gtt_ExecutorStart()"); /* Do not waste time here if the feature is not enabled for this session */ if (pgtt_is_enabled && NOT_IN_PARALLEL_WORKER) { /* Try to load pgtt if not already done. */ gtt_try_load(); /* check if we are working on a GTT and create it if it doesn't exist */ if (queryDesc->operation == CMD_INSERT || queryDesc->operation == CMD_DELETE || queryDesc->operation == CMD_UPDATE || queryDesc->operation == CMD_SELECT) { /* Verify if a GTT table is defined, create it if this is not already the case */ if (gtt_table_exists(queryDesc)) elog(DEBUG1, "ExecutorStart() statement use a Global Temporary Table"); } } elog(DEBUG1, "restore ExecutorStart()"); /* Continue the normal behavior */ if (prev_ExecutorStart) prev_ExecutorStart(queryDesc, eflags); else standard_ExecutorStart(queryDesc, eflags); elog(DEBUG1, "End of gtt_ExecutorStart()"); } static bool gtt_table_exists(QueryDesc *queryDesc) { bool is_gtt = false; char *name = NULL; char relpersistence; RangeTblEntry *rte; Relation rel; Gtt gtt; PlannedStmt *pstmt = (PlannedStmt *) queryDesc->plannedstmt; if (GttHashTable == NULL || !pstmt) return false; /* no relation in rtable probably a function call */ if (list_length(pstmt->rtable) == 0) return false; /* This must be a valid relation and not a temporary table */ rte = (RangeTblEntry *) linitial(pstmt->rtable); if (rte->relid != InvalidOid && rte->relkind == RELKIND_RELATION && !is_catalog_relid(rte->relid)) { #if (PG_VERSION_NUM >= 120000) rel = table_open(rte->relid, NoLock); #else rel = heap_open(rte->relid, NoLock); #endif name = RelationGetRelationName(rel); relpersistence = rel->rd_rel->relpersistence; #if (PG_VERSION_NUM >= 120000) table_close(rel, NoLock); #else heap_close(rel, NoLock); #endif /* Do not go further with temporary tables, catalog or toast table */ if (relpersistence != RELPERSISTENCE_TEMP) return false; gtt.relid = 0; gtt.temp_relid = 0; gtt.relname[0] = '\0'; gtt.preserved = false; gtt.code = NULL; gtt.created = false; /* Check if the table is in the hash list and it has not already be created */ if ((const void*)name != NULL) GttHashTableLookup(name, gtt); elog(DEBUG1, "gtt_table_exists() looking for table \"%s\" with relid %d into cache.", name, rte->relid); if (gtt.relname[0] != '\0') { elog(DEBUG1, "GTT found in cache with name: %s, relid: %d, temp_relid %d", gtt.relname, gtt.relid, gtt.temp_relid); /* Create the temporary table if it does not exists */ if (!gtt.created) { ParseState *pstate = make_parsestate(NULL); elog(DEBUG1, "global temporary table does not exists create it: %s", gtt.relname); /* Call create temporary table */ if ((gtt.temp_relid = create_temporary_table_internal(pstate, gtt.relid, gtt.preserved)) != InvalidOid) { elog(DEBUG1, "global temporary table %s (oid: %d) created", gtt.relname, gtt.temp_relid); /* Update hash list with table flagged as created */ gtt.created = true; GttHashTableDelete(gtt.relname); GttHashTableInsert(gtt, gtt.relname); free_parsestate(pstate); } else elog(ERROR, "can not create global temporary table %s", gtt.relname); } is_gtt = true; } else /* the table is not a global temporary table do nothing*/ elog(DEBUG1, "table \"%s\" not registered as GTT", name); } return is_gtt; } static bool is_declared_gtt(Oid relid) { char *name = NULL; char relpersistence; Relation rel; Gtt gtt; if (GttHashTable == NULL) return false; /* This must be a valid relation and not a temporary table */ if (relid != InvalidOid && !is_catalog_relid(relid)) { #if (PG_VERSION_NUM >= 120000) rel = table_open(relid, NoLock); #else rel = heap_open(relid, NoLock); #endif name = RelationGetRelationName(rel); relpersistence = rel->rd_rel->relpersistence; #if (PG_VERSION_NUM >= 120000) table_close(rel, NoLock); #else heap_close(rel, NoLock); #endif /* Do not go further with temporary tables, catalog or toast table */ if (relpersistence != RELPERSISTENCE_TEMP) return false; /* Check if the table is in the hash list and it has not already be created */ gtt.relid = 0; gtt.temp_relid = 0; gtt.relname[0] = '\0'; gtt.preserved = false; gtt.code = NULL; gtt.created = false; if ((const void*)name != NULL) GttHashTableLookup(name, gtt); if (gtt.relname[0] != '\0') return true; } return false; } int strpos(char *hay, char *needle, int offset) { char *haystack; char *p; haystack = (char *) malloc(strlen(hay)); if (haystack == NULL) { fprintf(stderr, "out of memory\n"); exit(EXIT_FAILURE); return -1; } memset(haystack, 0, strlen(hay)); strncpy(haystack, hay+offset, strlen(hay)-offset); p = strstr(haystack, needle); if (p) return p - haystack+offset; return -1; } /* * Create the Global Temporary Table with all associated objects * by creating the template table and register the GTT in the * pg_global_temp_tables table. * */ static Oid gtt_create_table_statement(Gtt gtt) { char *newQueryString = NULL; int connected = 0; int finished = 0; int result = 0; Oid gttOid = InvalidOid; Datum oidDatum; bool isnull; elog(DEBUG1, "proceeding to Global Temporary Table creation."); connected = SPI_connect(); if (connected != SPI_OK_CONNECT) ereport(ERROR, (errmsg("could not connect to SPI manager"))); /* Create the "template" table */ newQueryString = psprintf("CREATE UNLOGGED TABLE %s.%s (%s)", quote_identifier(pgtt_namespace_name), quote_identifier(gtt.relname), gtt.code); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Get Oid of the newly created table */ newQueryString = psprintf("SELECT c.relfilenode FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE c.relname=%s AND n.nspname = %s", quote_literal_cstr(gtt.relname), quote_literal_cstr(pgtt_namespace_name)); result = SPI_exec(newQueryString, 0); if (result != SPI_OK_SELECT && SPI_processed != 1) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); oidDatum = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull); if (!isnull) gttOid = DatumGetInt32(oidDatum); if (isnull || !OidIsValid(gttOid)) ereport(ERROR, (errmsg("can not get OID of newly created GTT template table %s", quote_identifier(gtt.relname)))); /* Now register the GTT table */ newQueryString = psprintf("INSERT INTO %s.pg_global_temp_tables VALUES (%d, %s, %s, '%c', %s)", quote_identifier(pgtt_namespace_name), gttOid, quote_literal_cstr(pgtt_namespace_name), quote_literal_cstr(gtt.relname), (gtt.preserved) ? 't' : 'f', quote_literal_cstr(gtt.code) ); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("can not registrer new global temporary table"))); /* * Set privilege on the unlogged table. Only SELECT is granted to * PUBLIC here (previously ALL was granted, which let any role * INSERT/UPDATE/DELETE/TRUNCATE data in this table, and attach * triggers to it, regardless of ownership). SELECT is kept so the * documented "SET pgtt.enabled TO off; SELECT * FROM ." * diagnostic pattern keeps working for any role. */ newQueryString = psprintf("GRANT SELECT ON TABLE %s.%s TO public", quote_identifier(pgtt_namespace_name), quote_identifier(gtt.relname)); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Mark the GTT as been created before register the table in the cache */ gtt.created = true; finished = SPI_finish(); if (finished != SPI_OK_FINISH) ereport(ERROR, (errmsg("could not disconnect from SPI manager"))); return gttOid; } /* * Unregister a Global Temporary Table in pg_global_temp_tables table * using his relid. */ static void gtt_unregister_global_temporary_table(const char *relname) { RangeVar *rv; Relation rel; ScanKeyData key[1]; SysScanDesc scan; HeapTuple tuple; elog(DEBUG1, "Looking for registered GTT relname = %s", relname); /* Set and open the GTT relation */ rv = makeRangeVar(pgtt_namespace_name, CATALOG_GLOBAL_TEMP_REL, -1); #if (PG_VERSION_NUM >= 120000) rel = table_openrv(rv, RowExclusiveLock); #else rel = heap_openrv(rv, RowExclusiveLock); #endif /* Define scanning */ ScanKeyInit(&key[0], Anum_pgtt_relname, BTEqualStrategyNumber, F_NAMEEQ, CStringGetDatum(relname)); /* Start search of relation */ scan = systable_beginscan(rel, 0, true, NULL, 1, key); /* Remove the tuples. */ while (HeapTupleIsValid(tuple = systable_getnext(scan))) { elog(DEBUG1, "removing tuple with relname = %s", relname); simple_heap_delete(rel, &tuple->t_self); } /* Cleanup. */ systable_endscan(scan); #if (PG_VERSION_NUM >= 120000) table_close(rel, RowExclusiveLock); #else heap_close(rel, RowExclusiveLock); #endif } /* * Unregister a Global Temporary Table in pg_global_temp_tables table * that is not cached and using his name only. */ static void gtt_unregister_gtt_not_cached(const char *relname) { RangeVar *rv; Relation rel; ScanKeyData key[1]; SysScanDesc scan; HeapTuple tuple; elog(DEBUG1, "Looking for registered GTT relname = %s", relname); /* Set and open the GTT relation */ rv = makeRangeVar(pgtt_namespace_name, CATALOG_GLOBAL_TEMP_REL, -1); #if (PG_VERSION_NUM >= 120000) rel = table_openrv(rv, RowExclusiveLock); #else rel = heap_openrv(rv, RowExclusiveLock); #endif /* Define scanning */ ScanKeyInit(&key[0], Anum_pgtt_relname, BTEqualStrategyNumber, F_NAMEEQ, CStringGetDatum(relname)); /* Start search of relation */ scan = systable_beginscan(rel, 0, true, NULL, 1, key); /* * Remove the tuples, but only those the current user is actually * allowed to drop. Previously this deleted any matching row * unconditionally, before PostgreSQL's own ownership check on the * subsequent DROP TABLE ever ran -- letting any authenticated role * deregister another user's Global Temporary Table simply by * naming it in a DROP TABLE statement, even though that statement * would itself go on to fail with a permission error. */ while (HeapTupleIsValid(tuple = systable_getnext(scan))) { bool isnull; Datum relidDatum = heap_getattr(tuple, Anum_pgtt_relid, RelationGetDescr(rel), &isnull); if (isnull || !gtt_current_user_can_drop(DatumGetInt32(relidDatum))) { systable_endscan(scan); #if (PG_VERSION_NUM >= 120000) table_close(rel, RowExclusiveLock); #else heap_close(rel, RowExclusiveLock); #endif ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be owner of Global Temporary Table \"%s\" to drop it", relname))); } elog(DEBUG1, "removing tuple with relname = %s", relname); simple_heap_delete(rel, &tuple->t_self); } /* Cleanup. */ systable_endscan(scan); #if (PG_VERSION_NUM >= 120000) table_close(rel, RowExclusiveLock); #else heap_close(rel, RowExclusiveLock); #endif } /* * Check if pgtt hasn't been loaded yet, and try to load it in that case. */ static void gtt_try_load(void) { /* * Don't try to load if the extension is disabled, if we can't do it now or * if it's already loaded. */ if (!pgtt_is_enabled || !IsTransactionState() || GttHashTable != NULL) return; /* Initialize list of Global Temporary Table */ if (EnableGttManager()) { /* * Load temporary table definition from pg_global_temp_tables table * into our Hash table and pre-create the temporary tables. */ gtt_load_global_temporary_tables(); /* * Be sure that extension schema is at end of the search path so that * "template" tables will be found. */ force_pgtt_namespace(); } } #if PG_VERSION_NUM < 160000 /* * From src/backend/commands/extension.c */ Oid get_extension_schema(Oid ext_oid) { Oid result; Relation rel; SysScanDesc scandesc; HeapTuple tuple; ScanKeyData entry[1]; #if (PG_VERSION_NUM >= 120000) rel = table_open(ExtensionRelationId, AccessShareLock); #else rel = heap_open(ExtensionRelationId, AccessShareLock); #endif ScanKeyInit(&entry[0], #if (PG_VERSION_NUM >= 120000) Anum_pg_extension_oid, #else ObjectIdAttributeNumber, #endif BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(ext_oid)); scandesc = systable_beginscan(rel, ExtensionOidIndexId, true, NULL, 1, entry); tuple = systable_getnext(scandesc); /* We assume that there can be at most one matching tuple */ if (HeapTupleIsValid(tuple)) result = ((Form_pg_extension) GETSTRUCT(tuple))->extnamespace; else result = InvalidOid; systable_endscan(scandesc); #if (PG_VERSION_NUM >= 120000) table_close(rel, AccessShareLock); #else heap_close(rel, AccessShareLock); #endif return result; } #endif /* * EnableGttManager * Enables the GTT management cache at backend startup. */ bool EnableGttManager(void) { Oid extOid = get_extension_oid("pgtt", true); RangeVar *rv; char *nspname; if (!OidIsValid(extOid)) return false; pgtt_namespace_oid = get_extension_schema(extOid); if (!OidIsValid(pgtt_namespace_oid)) elog(ERROR, "namespace %d can not be found.", pgtt_namespace_oid); /* * Check if the GTT relation also exist. We might be in the middle of the * extension creation, where the line in pg_extension exists but not the * rest of SQL objects. */ nspname = get_namespace_name(pgtt_namespace_oid); rv = makeRangeVar(nspname, CATALOG_GLOBAL_TEMP_REL, -1); if (!OidIsValid(RangeVarGetRelid(rv, AccessShareLock, true))) return false; if (GttHashTable == NULL) { HASHCTL ctl; MemSet(&ctl, 0, sizeof(ctl)); ctl.keysize = NAMEDATALEN; ctl.entrysize = sizeof(GttHashEnt); /* allocate GTT Cache in the cache context */ ctl.hcxt = CacheMemoryContext; GttHashTable = hash_create("Global Temporary Table hash list", GTT_PER_DATABASE, &ctl, #if PG_VERSION_NUM >= 140000 HASH_STRINGS | HASH_ELEM | HASH_CONTEXT #else HASH_ELEM | HASH_CONTEXT #endif ); elog(DEBUG1, "GTT cache initialized."); } /* * Set the OID and name of the extension schema, all objects will be * created in this schema. */ strlcpy(pgtt_namespace_name, nspname, sizeof(pgtt_namespace_name)); return true; } /* * Delete all declared Global Temporary Table. * */ void GttHashTableDeleteAll(void) { HASH_SEQ_STATUS status; GttHashEnt *lentry = NULL; if (GttHashTable == NULL) return; hash_seq_init(&status, GttHashTable); while ((lentry = (GttHashEnt *) hash_seq_search(&status)) != NULL) { Gtt gtt = GetGttByName(lentry->name); elog(DEBUG1, "Remove GTT %s from our hash table", gtt.relname); GttHashTableDelete(lentry->name); /* Restart the iteration in case that led to other drops */ hash_seq_term(&status); hash_seq_init(&status, GttHashTable); } } /* * GetGttByName * Returns a Gtt given a table name, or NULL if name is not found. * * Caller should have made sure that GTT has been properly loaded before * calling this function. */ Gtt GetGttByName(const char *name) { Gtt gtt; Assert(GttHashTable != NULL); if ((const void*)name != NULL) GttHashTableLookup(name, gtt); return gtt; } /* * Execute a utility statement generated by pgtt as a sub-command of the * statement being processed. */ static void gtt_exec_utility_subcommand(Node *stmt, const char *querystring) { #if PG_VERSION_NUM >= 100000 PlannedStmt *pstmt = makeNode(PlannedStmt); pstmt->commandType = CMD_UTILITY; pstmt->canSetTag = true; pstmt->utilityStmt = stmt; pstmt->stmt_location = -1; pstmt->stmt_len = 0; ProcessUtility(pstmt, querystring, #if PG_VERSION_NUM >= 140000 false, #endif PROCESS_UTILITY_SUBCOMMAND, NULL, NULL, None_Receiver, NULL); #else ProcessUtility(stmt, querystring, PROCESS_UTILITY_SUBCOMMAND, NULL, None_Receiver, NULL); #endif } /* * Definition of a trigger to be copied on the temporary table. */ typedef struct GttTriggerDef { char *def; /* CREATE TRIGGER statement */ char *tgname; /* name of the trigger */ char tgenabled; /* tgenabled state on the "template" table */ } GttTriggerDef; /* * Restore on the temporary table the enabled state of a trigger when it * is not the default one. pg_get_triggerdef() does not report the * tgenabled state so the trigger is always created as ENABLE (fires on * origin) and an ALTER TABLE sub-command is required to fix it. */ static void gtt_set_trigger_enabled(const char *relname, const char *tgname, char tgenabled) { AlterTableStmt *atstmt; AlterTableCmd *atcmd; const char *action = NULL; atcmd = makeNode(AlterTableCmd); switch (tgenabled) { case TRIGGER_DISABLED: atcmd->subtype = AT_DisableTrig; action = "DISABLE TRIGGER"; break; case TRIGGER_FIRES_ON_REPLICA: atcmd->subtype = AT_EnableReplicaTrig; action = "ENABLE REPLICA TRIGGER"; break; case TRIGGER_FIRES_ALWAYS: atcmd->subtype = AT_EnableAlwaysTrig; action = "ENABLE ALWAYS TRIGGER"; break; default: /* TRIGGER_FIRES_ON_ORIGIN is the state set at creation time */ return; } atcmd->name = pstrdup(tgname); atcmd->behavior = DROP_RESTRICT; atcmd->missing_ok = false; atstmt = makeNode(AlterTableStmt); atstmt->relation = makeRangeVar("pg_temp", pstrdup(relname), -1); atstmt->relation->relpersistence = RELPERSISTENCE_TEMP; atstmt->cmds = list_make1(atcmd); atstmt->missing_ok = false; STMT_OBJTYPE(atstmt) = OBJECT_TABLE; elog(DEBUG1, "restoring state of trigger \"%s\" on temporary table \"%s\": %s", tgname, relname, action); gtt_exec_utility_subcommand((Node *) atstmt, "PGTT provide a query string"); CommandCounterIncrement(); } /* * Copy on the temporary table the triggers defined on the "template" * table of a global temporary table. * * CREATE TABLE ... (LIKE ...) has no INCLUDING TRIGGERS option in * PostgreSQL, so the triggers must be replicated by hand once the * temporary table has been created. The definition of each trigger is * obtained through pg_get_triggerdef(), parsed back, and the relation * of the resulting CreateTrigStmt is changed to point to the temporary * table before being executed. * * See https://github.com/darold/pgtt/issues/52 */ static void gtt_copy_triggers(Oid parent_relid, const char *relname) { Relation tgrel; SysScanDesc tgscan; ScanKeyData key; HeapTuple tuple; List *trigger_defs = NIL; ListCell *lc; elog(DEBUG1, "looking for triggers to copy from relation with Oid %d", parent_relid); /* * Collect the definition of all the triggers of the "template" table * first, the catalog scan must be closed before executing any DDL. */ tgrel = table_open(TriggerRelationId, AccessShareLock); ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(parent_relid)); tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true, NULL, 1, &key); while (HeapTupleIsValid(tuple = systable_getnext(tgscan))) { Form_pg_trigger trigform = (Form_pg_trigger) GETSTRUCT(tuple); GttTriggerDef *trigdef; Oid trigoid; Datum def; /* * Skip triggers created internally by PostgreSQL, they are built * again with the constraints and indexes copied by the LIKE clause. */ if (trigform->tgisinternal) continue; #if (PG_VERSION_NUM >= 120000) trigoid = trigform->oid; #else trigoid = HeapTupleGetOid(tuple); #endif #if (PG_VERSION_NUM < 140000) def = OidFunctionCall1(F_PG_GET_TRIGGERDEF, ObjectIdGetDatum(trigoid)); #else def = OidFunctionCall1(F_PG_GET_TRIGGERDEF_OID, ObjectIdGetDatum(trigoid)); #endif trigdef = (GttTriggerDef *) palloc0(sizeof(GttTriggerDef)); trigdef->def = TextDatumGetCString(def); trigdef->tgname = pstrdup(NameStr(trigform->tgname)); trigdef->tgenabled = trigform->tgenabled; trigger_defs = lappend(trigger_defs, trigdef); } systable_endscan(tgscan); table_close(tgrel, AccessShareLock); if (trigger_defs == NIL) return; /* The temporary table must be visible before adding the triggers */ CommandCounterIncrement(); foreach (lc, trigger_defs) { GttTriggerDef *trigdef = (GttTriggerDef *) lfirst(lc); List *raw_parsetree_list; ListCell *lc2; elog(DEBUG1, "copying trigger on temporary table: %s", trigdef->def); #if (PG_VERSION_NUM >= 140000) raw_parsetree_list = raw_parser(trigdef->def, RAW_PARSE_DEFAULT); #else raw_parsetree_list = raw_parser(trigdef->def); #endif foreach (lc2, raw_parsetree_list) { CreateTrigStmt *trigstmt; #if (PG_VERSION_NUM >= 100000) Node *parsetree = ((RawStmt *) lfirst(lc2))->stmt; #else Node *parsetree = (Node *) lfirst(lc2); #endif if (!IsA(parsetree, CreateTrigStmt)) continue; trigstmt = (CreateTrigStmt *) parsetree; /* Redirect the trigger creation to the temporary table */ trigstmt->relation = makeRangeVar("pg_temp", pstrdup(relname), -1); trigstmt->relation->relpersistence = RELPERSISTENCE_TEMP; gtt_exec_utility_subcommand(parsetree, trigdef->def); CommandCounterIncrement(); } /* * The trigger is always created enabled, restore the state it has * on the "template" table when it is a different one. */ gtt_set_trigger_enabled(relname, trigdef->tgname, trigdef->tgenabled); } } /* * Load Global Temporary Table in memory from pg_global_temp_tables table. */ static void gtt_load_global_temporary_tables(void) { RangeVar *rv; Relation rel; #if (PG_VERSION_NUM >= 120000) TableScanDesc scan; #else HeapScanDesc scan; #endif HeapTuple tuple; int numberOfAttributes; TupleDesc tupleDesc; Snapshot snapshot; elog(DEBUG1, "gtt_load_global_temporary_tables()"); elog(DEBUG1, "retrieve GTT list from definition table %s.%s", pgtt_namespace_name, CATALOG_GLOBAL_TEMP_REL); /* Set and open the GTT definition storage relation */ rv = makeRangeVar(pgtt_namespace_name, CATALOG_GLOBAL_TEMP_REL, -1); /* Open the CATALOG_GLOBAL_TEMP_REL table. We don't want to allow * writable accesses by other session during import. */ PushActiveSnapshot(GetTransactionSnapshot()); snapshot = GetActiveSnapshot(); #if (PG_VERSION_NUM >= 120000) rel = table_openrv(rv, AccessShareLock); #if (PG_VERSION_NUM >= 190000) scan = table_beginscan(rel, snapshot, 0, (ScanKey) NULL, SO_NONE); #else scan = table_beginscan(rel, snapshot, 0, (ScanKey) NULL); #endif #else rel = heap_openrv(rv, AccessShareLock); scan = heap_beginscan(rel, snapshot, 0, (ScanKey) NULL); #endif tupleDesc = RelationGetDescr(rel); numberOfAttributes = tupleDesc->natts; while (HeapTupleIsValid(tuple = heap_getnext(scan, ForwardScanDirection))) { Oid namespaceId; Gtt gtt; Datum *values = (Datum *) palloc(numberOfAttributes * sizeof(Datum)); bool *isnull = (bool *) palloc(numberOfAttributes * sizeof(bool)); /* Extract data */ heap_deform_tuple(tuple, tupleDesc, values, isnull); strlcpy(gtt.relname, NameStr(*(DatumGetName(values[2]))), sizeof(gtt.relname)); gtt.preserved = DatumGetBool(values[3]); gtt.code = TextDatumGetCString(values[4]); gtt.created = false; gtt.temp_relid = 0; /* get relation id */ namespaceId = LookupExplicitNamespace(pgtt_namespace_name, false); gtt.relid = get_relname_relid(gtt.relname, namespaceId); /* Add table to cache */ GttHashTableInsert(gtt, gtt.relname); } /* Cleanup. */ #if (PG_VERSION_NUM >= 120000) table_endscan(scan); table_close(rel, AccessShareLock); #else heap_endscan(scan); heap_close(rel, AccessShareLock); #endif PopActiveSnapshot(); } static Oid create_temporary_table_internal(ParseState *pstate, Oid parent_relid, bool preserved) { /* Value to be returned */ Oid temp_relid = InvalidOid; /* safety */ #if (PG_VERSION_NUM >= 130000) ObjectAddress address; #endif /* Parent's namespace and name */ Oid parent_nsp; char *parent_name, *parent_nsp_name; char parent_persistence; /* Elements of the "CREATE TABLE" query tree */ RangeVar *parent_rv; RangeVar *table_rv; TableLikeClause *like_clause = makeNode(TableLikeClause); CreateStmt *createStmt = makeNode(CreateStmt); List *createStmts; ListCell *lc; elog(DEBUG1, "creating a temporary table like table with Oid %d", parent_relid); /* Lock parent and check if it exists */ LockRelationOid(parent_relid, ShareUpdateExclusiveLock); if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(parent_relid))) elog(ERROR, "relation %u does not exist", parent_relid); /* Cache parent's namespace and name */ parent_name = get_rel_name(parent_relid); parent_nsp = get_rel_namespace(parent_relid); parent_nsp_name = get_namespace_name(parent_nsp); parent_persistence = get_rel_persistence(parent_relid); /* Make up parent's RangeVar */ parent_rv = makeRangeVar(parent_nsp_name, parent_name, -1); parent_rv->relpersistence = parent_persistence; elog(DEBUG1, "Parent namespace: %s, parent relname: %s, parent oid: %d", parent_rv->schemaname, parent_rv->relname, parent_relid); /* Set name of temporary table same as parent table */ table_rv = makeRangeVar("pg_temp", parent_rv->relname, -1); Assert(table_rv); elog(DEBUG1, "Initialize TableLikeClause structure"); /* Initialize TableLikeClause structure */ like_clause->relation = copyObject(parent_rv); like_clause->options = CREATE_TABLE_LIKE_DEFAULTS | CREATE_TABLE_LIKE_INDEXES | CREATE_TABLE_LIKE_CONSTRAINTS #if (PG_VERSION_NUM >= 100000) | CREATE_TABLE_LIKE_IDENTITY #endif #if (PG_VERSION_NUM >= 120000) | CREATE_TABLE_LIKE_GENERATED #endif | CREATE_TABLE_LIKE_COMMENTS; elog(DEBUG1, "Initialize CreateStmt structure"); /* Initialize CreateStmt structure */ createStmt->relation = copyObject(table_rv); createStmt->relation->schemaname = NULL; createStmt->relation->relpersistence = RELPERSISTENCE_TEMP; createStmt->tableElts = list_make1(copyObject(like_clause)); createStmt->inhRelations = NIL; createStmt->ofTypename = NULL; createStmt->constraints = NIL; createStmt->options = NIL; #if (PG_VERSION_NUM >= 120000) createStmt->accessMethod = NULL; #endif if (preserved) createStmt->oncommit = ONCOMMIT_PRESERVE_ROWS; else createStmt->oncommit = ONCOMMIT_DELETE_ROWS; createStmt->tablespacename = NULL; createStmt->if_not_exists = false; elog(DEBUG1, "Obtain the sequence of Stmts to create temporary table"); /* Obtain the sequence of Stmts to create temporary table */ createStmts = transformCreateStmt(createStmt, NULL); elog(DEBUG1, "Processing list of statements"); /* Create the temporary table */ foreach (lc, createStmts) { /* Fetch current CreateStmt */ Node *cur_stmt = (Node *) lfirst(lc); elog(DEBUG1, "Processing statement of type %d", nodeTag(cur_stmt)); if (IsA(cur_stmt, CreateStmt)) { Datum toast_options; #if PG_VERSION_NUM < 180000 static char *validnsps[] = HEAP_RELOPT_NAMESPACES; #else const char *validnsps[] = HEAP_RELOPT_NAMESPACES; #endif Oid temp_relowner; /* Temporary table owner must be current user */ temp_relowner = GetUserId(); elog(DEBUG1, "Creating a temporary table and get its Oid"); /* Create a temporary table and save its Oid */ #if (PG_VERSION_NUM < 100000) temp_relid = DefineRelation((CreateStmt *) cur_stmt, RELKIND_RELATION, temp_relowner, NULL).objectId; #elif (PG_VERSION_NUM < 130000) temp_relid = DefineRelation((CreateStmt *) cur_stmt, RELKIND_RELATION, temp_relowner, NULL, NULL).objectId; #else address = DefineRelation((CreateStmt *) cur_stmt, RELKIND_RELATION, temp_relowner, NULL, NULL); temp_relid = address.objectId; #endif /* Update config one more time */ CommandCounterIncrement(); /* * parse and validate reloptions for the toast * table */ toast_options = transformRelOptions((Datum) 0, ((CreateStmt *) cur_stmt)->options, "toast", validnsps, true, false); (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true); NewRelationCreateToastTable(temp_relid, toast_options); } else if (IsA(cur_stmt, IndexStmt)) { Oid relid; elog(DEBUG1, "execution statement CREATE INDEX, relation has an index."); relid = RangeVarGetRelidExtended(((IndexStmt *) cur_stmt)->relation, ShareLock, #if (PG_VERSION_NUM >= 110000) 0, #else false, false, #endif RangeVarCallbackOwnsRelation, NULL); DefineIndex( #if (PG_VERSION_NUM >= 190000) pstate, #endif relid, /* OID of heap relation */ (IndexStmt *) cur_stmt, InvalidOid, /* no predefined OID */ #if (PG_VERSION_NUM >= 110000) InvalidOid, /* no parent index */ InvalidOid, /* no parent constraint */ #endif #if (PG_VERSION_NUM >= 160000) -1,/* total parts */ #endif false, /* is_alter_table */ true, /* check_rights */ #if (PG_VERSION_NUM > 100000) true, /* check_not_in_use */ #endif false, /* skip_build */ false); /* quiet */ } else if (IsA(cur_stmt, CommentStmt)) { CommentObject((CommentStmt *) cur_stmt); } #if (PG_VERSION_NUM >= 90600) else if (IsA(cur_stmt, TableLikeClause)) { TableLikeClause *like = (TableLikeClause *) cur_stmt; RangeVar *rv = createStmt->relation; List *morestmts; morestmts = expandTableLikeClause(rv, like); createStmts = list_concat(createStmts, morestmts); /* don't need a CCI now */ continue; } #endif else { /* * Recurse for anything else. */ gtt_exec_utility_subcommand(cur_stmt, "PGTT provide a query string"); } /* Need CCI between commands */ #if (PG_VERSION_NUM < 130000) if (lnext(lc) != NULL) #else if (lnext(createStmts, lc) != NULL) #endif CommandCounterIncrement(); } /* * CREATE TABLE ... (LIKE ...) does not copy the triggers, they must be * replicated on the temporary table by hand. See issue #52. */ if (OidIsValid(temp_relid)) gtt_copy_triggers(parent_relid, parent_rv->relname); /* release lock on "template" relation */ UnlockRelationOid(parent_relid, ShareUpdateExclusiveLock); elog(DEBUG1, "Create a temporary table done with Oid: %d", temp_relid); return temp_relid; } /* * Post-parse-analysis hook: mark query with a queryId */ #if PG_VERSION_NUM >= 190000 static void gtt_post_parse_analyze(ParseState *pstate, Query *query, const JumbleState *jstate) #else #if PG_VERSION_NUM >= 140000 static void gtt_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) #else static void gtt_post_parse_analyze(ParseState *pstate, Query *query) #endif #endif { /* Try to load pgtt if not already done. */ gtt_try_load(); /* * Reroute all the references to a GTT "template" table found in the * query tree, including the ones in sub-queries, CTE and sub-links, * not just the first entry of the top level range table. */ if (NOT_IN_PARALLEL_WORKER && pgtt_is_enabled && GttHashTable != NULL) gtt_rewrite_query(pstate, query); /* restore hook */ if (prev_post_parse_analyze_hook) { #if PG_VERSION_NUM >= 140000 prev_post_parse_analyze_hook(pstate, query, jstate); #else prev_post_parse_analyze_hook(pstate, query); #endif } } /* * Reroute a single range table entry to the temporary table backing the * global temporary table, when the relation is a GTT "template" table. * The temporary table is created on the fly if it does not exist yet. */ static void gtt_rewrite_rte(ParseState *pstate, Query *query, RangeTblEntry *rte) { Relation rel; Gtt gtt; char *name = NULL; /* This must be a valid relation not from pg_catalog */ if (rte->rtekind != RTE_RELATION || rte->relid == InvalidOid || rte->relkind != RELKIND_RELATION || is_catalog_relid(rte->relid)) return; #if (PG_VERSION_NUM >= 120000) rel = table_open(rte->relid, NoLock); #else rel = heap_open(rte->relid, NoLock); #endif name = RelationGetRelationName(rel); #if (PG_VERSION_NUM >= 120000) table_close(rel, NoLock); #else heap_close(rel, NoLock); #endif gtt.relid = 0; gtt.temp_relid = 0; gtt.relname[0] = '\0'; gtt.preserved = false; gtt.code = NULL; gtt.created = false; /* Check if the table is in the hash list and it has not already be created */ if ((const void*)name != NULL) { elog(DEBUG1, "gtt_rewrite_rte() looking for table \"%s\" with relid %d into cache.", name, rte->relid); GttHashTableLookup(name, gtt); } else elog(ERROR, "gtt_rewrite_rte() table to search in cache is not valide pointer, relid: %d.", rte->relid); if (gtt.relname[0] == '\0') { /* the table is not a global temporary table do nothing*/ elog(DEBUG1, "table \"%s\" not registered as GTT", name); return; } /* After an error and rollback the table is still registered in cache but must be initialized */ if (gtt.created && OidIsValid(gtt.temp_relid) && !SearchSysCacheExists1(RELOID, ObjectIdGetDatum(gtt.temp_relid)) ) { elog(DEBUG1, "invalid temporary table with relid %d (%s), reseting.", gtt.temp_relid, gtt.relname); gtt.created = false; gtt.temp_relid = 0; } /* Create the temporary table if it does not exists */ if (!gtt.created) { elog(DEBUG1, "global temporary table from relid %d does not exists create it: %s", rte->relid, gtt.relname); /* Call create temporary table */ if ((gtt.temp_relid = create_temporary_table_internal(pstate, gtt.relid, gtt.preserved)) != InvalidOid) { elog(DEBUG1, "global temporary table %s (oid: %d) created", gtt.relname, gtt.temp_relid); /* Update hash list with table flagged as created*/ gtt.created = true; GttHashTableDelete(gtt.relname); GttHashTableInsert(gtt, gtt.relname); } else elog(ERROR, "can not create global temporary table %s", gtt.relname); } elog(DEBUG1, "temporary table exists with oid %d", gtt.temp_relid); if (rte->relid != gtt.temp_relid) { #if PG_VERSION_NUM >= 160000 /* * Some RTE do not have any permission info attached, only fix the * relation Oid of the permission info when there is one. */ if (rte->perminfoindex > 0) { RTEPermissionInfo *rteperm = list_nth(query->rteperminfos, rte->perminfoindex - 1); rteperm->relid = gtt.temp_relid; } #endif LockRelationOid(gtt.temp_relid, rte->rellockmode); if (rte->rellockmode != AccessShareLock) UnlockRelationOid(rte->relid, rte->rellockmode); elog(DEBUG1, "rerouting relid %d access to %d for GTT table \"%s\"", rte->relid, gtt.temp_relid, name); rte->relid = gtt.temp_relid; } } /* * Reroute all the GTT "template" table references of a query and of all * its sub-queries (sub-selects, sub-links, CTE, set operations, ...). * * The previous implementation was only looking at the first entry of the * top level range table, so that a GTT referenced in a sub-query or in a * secondary FROM item was still reading the empty "template" table. See * https://github.com/darold/pgtt/issues/58 */ static void gtt_rewrite_query(ParseState *pstate, Query *query) { ListCell *lc; if (query == NULL) return; foreach(lc, query->rtable) gtt_rewrite_rte(pstate, query, (RangeTblEntry *) lfirst(lc)); /* Recurse into any sub-query of this query */ (void) query_tree_walker(query, gtt_query_walker, (void *) pstate, 0); } /* * Expression tree walker used to find the sub-queries of a query. * Utility statements are not walked by query_tree_walker(). */ static bool gtt_query_walker(Node *node, void *context) { if (node == NULL) return false; if (IsA(node, Query)) { /* gtt_rewrite_query() takes care of the recursion by itself */ gtt_rewrite_query((ParseState *) context, (Query *) node); return false; } return expression_tree_walker(node, gtt_query_walker, context); } static bool is_catalog_relid(Oid relid) { HeapTuple reltup; Form_pg_class relform; Oid relnamespace; reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(reltup)) elog(ERROR, "cache lookup failed for relation %u", relid); relform = (Form_pg_class) GETSTRUCT(reltup); relnamespace = relform->relnamespace; ReleaseSysCache(reltup); if (relnamespace == PG_CATALOG_NAMESPACE || relnamespace == PG_TOAST_NAMESPACE) { elog(DEBUG1, "relation %d is in pg_catalog or pg_toast schema, nothing to do.", relid); return true; } return false; } /* * Be sure that extension schema is at end of the search path so that * "template" tables will be found. */ static void force_pgtt_namespace(void) { bool override = false; char *old_search_path; const char *pgtt_schema = quote_identifier(pgtt_namespace_name); StringInfoData search_path; if (!IsTransactionState() || GttHashTable == NULL) return; initStringInfo(&search_path); old_search_path = GetConfigOptionByName("search_path", NULL, false); if (old_search_path == NULL) { appendStringInfo(&search_path, "%s", pgtt_schema); override = true; } else if (strlen(old_search_path) > 0 && strstr(old_search_path, pgtt_schema) == NULL) { /* * first look if pg_catalog is at end of the search path to keep * it at end. This is not the same behavior if it not kept at the * end when there is pg_catalog functions overloaded in another * schema */ bool at_end = false; int len = strlen(old_search_path) - 10; char *p = strstr(old_search_path, "pg_catalog"); if (p != NULL && strcmp(p, "pg_catalog") == 0) { /* remove redundant whitespaces */ while (len > 0 && isspace(old_search_path[len - 1])) len--; old_search_path[len] = '\0'; appendStringInfo(&search_path, "%s %s", old_search_path, pgtt_schema); at_end = true; } else { appendStringInfo(&search_path, "%s, %s", old_search_path, pgtt_schema); } if (at_end) appendStringInfo(&search_path, ", pg_catalog"); override = true; } if (override) { /* Override the search_path by adding our pgtt schema. */ SetConfigOption("search_path", search_path.data, (superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION); elog(DEBUG1, "search_path forced to %s.", search_path.data); } } /* * Update a registered Global Temporary Table * in the pg_global_temp_tables table. * */ static void gtt_update_registered_table(Gtt gtt) { char *newQueryString = NULL; int connected = 0; int finished = 0; int result = 0; elog(DEBUG1, "proceeding to Global Temporary Table modification."); connected = SPI_connect(); if (connected != SPI_OK_CONNECT) ereport(ERROR, (errmsg("could not connect to SPI manager"))); newQueryString = psprintf("UPDATE %s.pg_global_temp_tables SET relname = %s WHERE relid = %d", quote_identifier(pgtt_namespace_name), quote_literal_cstr(gtt.relname), gtt.relid ); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("can not update relid %d into %s.pg_global_temp_tables", gtt.relid, quote_identifier(pgtt_namespace_name)))); finished = SPI_finish(); if (finished != SPI_OK_FINISH) ereport(ERROR, (errmsg("could not disconnect from SPI manager"))); } /* * Create the temporary table related to a Global Temporary Table * and register the GTT in pg_global_temp_tables table. * */ static void gtt_create_table_as(Gtt gtt, bool skipdata) { char *newQueryString = NULL; int connected = 0; int finished = 0; int result = 0; Oid gttOid = InvalidOid; Datum oidDatum; bool isnull; elog(DEBUG1, "proceeding to Global Temporary Table creation."); /* This can only be called if GTT has been properly loaded. */ Assert(GttHashTable != NULL); connected = SPI_connect(); if (connected != SPI_OK_CONNECT) ereport(ERROR, (errmsg("could not connect to SPI manager"))); /* Create the "template" table */ newQueryString = psprintf("CREATE UNLOGGED TABLE %s.%s %s;", quote_identifier(pgtt_namespace_name), quote_identifier(gtt.relname), gtt.code); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Get Oid of the newly created table */ newQueryString = psprintf("SELECT c.relfilenode FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE c.relname=%s AND n.nspname = %s", quote_literal_cstr(gtt.relname), quote_literal_cstr(pgtt_namespace_name)); result = SPI_exec(newQueryString, 0); if (result != SPI_OK_SELECT && SPI_processed != 1) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); oidDatum = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull); if (!isnull) gttOid = DatumGetInt32(oidDatum); if (isnull || !OidIsValid(gttOid)) ereport(ERROR, (errmsg("can not get OID of newly created GTT template table %s", quote_identifier(gtt.relname)))); gtt.relid = gttOid; /* Create the temporary table only if data from source table must be inserted */ if (!skipdata) { char namespaceName[NAMEDATALEN]; /* Get current temporary namespace name */ snprintf(namespaceName, sizeof(namespaceName), "pg_temp_%d", #if PG_VERSION_NUM < 170000 MyBackendId #else MyProcNumber #endif ); newQueryString = psprintf("CREATE TEMPORARY TABLE %s %s WITH DATA", quote_identifier(gtt.relname), gtt.code); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); /* Get Oid of the newly created temporary table */ newQueryString = psprintf("SELECT c.relfilenode FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE c.relname=%s AND n.nspname = %s", quote_literal_cstr(gtt.relname), quote_literal_cstr(namespaceName)); result = SPI_exec(newQueryString, 0); if (result != SPI_OK_SELECT && SPI_processed != 1) ereport(ERROR, (errmsg("execution failure on query: \"%s\"", newQueryString))); oidDatum = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull); if (!isnull) gtt.temp_relid = DatumGetInt32(oidDatum); if (isnull || !OidIsValid(gttOid)) ereport(ERROR, (errmsg("can not get OID of newly created temporary table %s", quote_identifier(gtt.relname)))); gtt.created = true; } /* Now register the GTT table */ newQueryString = psprintf("INSERT INTO %s.pg_global_temp_tables VALUES (%d, %s, %s, '%c', %s)", quote_identifier(pgtt_namespace_name), gtt.relid, quote_literal_cstr(pgtt_namespace_name), quote_literal_cstr(gtt.relname), (gtt.preserved) ? 't' : 'f', quote_literal_cstr(gtt.code) ); result = SPI_exec(newQueryString, 0); if (result < 0) ereport(ERROR, (errmsg("can not registrer new global temporary table"))); finished = SPI_finish(); if (finished != SPI_OK_FINISH) ereport(ERROR, (errmsg("could not disconnect from SPI manager"))); /* registrer the table in the cache */ GttHashTableDelete(gtt.relname); GttHashTableInsert(gtt, gtt.relname); } int strremovestr(char *src, char *toremove) { while( *src ) { char *k=toremove,*s=src; while( *k && *k==*s ) ++k,++s; if( !*k ) { while( *s ) *src++=*s++; *src=0; return 1; } ++src; } return 0; } darold-pgtt-fe8b2d5/pgtt.control000066400000000000000000000002611522714605300170200ustar00rootroot00000000000000default_version = '4.6.0' comment = 'Extension to add Global Temporary Tables feature to PostgreSQL' module_pathname = '$libdir/pgtt' schema = 'pgtt_schema' relocatable = false darold-pgtt-fe8b2d5/pgtt.md000066400000000000000000000440741522714605300157520ustar00rootroot00000000000000* [Description](#description) * [Installation](#installation) * [Configuration](#configuration) * [Use of the extension](#use-of-the-extension) * [How the extension really works](#how-the-extension-really-works) * [Performances](performances) * [Authors](#authors) ## PostgreSQL Global Temporary Tables ### [Description](#description) pgtt is a PostgreSQL extension to create, manage and use Oracle-style Global Temporary Tables and the others RDBMS. The objective of this extension it to provide the Global Temporary Table feature to PostgreSQL waiting for an in core implementation. The main interest of this extension is to mimic the Oracle behavior with GTT when you can not or don't want to rewrite the application code when migrating to PostgreSQL. In all other case best is to rewrite the code to use standard PostgreSQL temporary tables. This version of the GTT extension use a regular unlogged table as "template" table and an internal rerouting to a temporary table. See chapter "How the extension really works" for more details. A previous implementation of this extension using Row Security Level is still available [here](https://github.com/darold/pgtt-rsl). PostgreSQL native temporary tables are automatically dropped at the end of a session, or optionally at the end of the current transaction. Global Temporary Tables (GTT) are permanent, they are created as regular tables visible to all users but their content is relative to the current session or transaction. Even if the table is persistent a session or transaction can not see rows written by an other session. Usually this is not a problem, you have learn to deal with the temporary table behavior of PostgreSQL but the problem comes when you are migrating an Oracle database to PostgreSQL. You have to rewrite the SQL and PlPgSQL code to follow the application logic and use PostgreSQL temporary table, that mean recreating the temporary table everywhere it is used. The other advantage of this kind of object is when your application creates and drops a lot of temporary tables, the PostgreSQL catalogs becomes bloated and the performances start to fall. Usually Global Temporary Tables prevent catalog bloating, but with this implementation and even if we have a permanent table, all DML are rerouted to a regular temporary table created at first access. See below chapter "How the extension really works" for more information. DECLARE TEMPORARY TABLE statement is not supported by PostgreSQL and by this extension. However this statement defines a temporary table for the current connection / session, it creates tables that do not reside in the system catalogs and are not persistent. It cannot be shared with other sessions. This is the equivalent of PostgreSQL standard CREATE TEMPORARY TABLE so you might just have to replace the DECLARE keyword by CREATE. All Oracle's GTT behavior are respected with the different clauses minus what is not supported by PostgreSQL: #### ON COMMIT {DELETE | PRESERVE} ROWS Specifies the action taken on the global temporary table when a COMMIT operation is performed. - DELETE ROWS: all rows of the table will be deleted if no holdable cursor is open on the table. - PRESERVE ROWS: the rows of the table will be preserved after the COMMIT. #### LOGGED or NOT LOGGED [ ON ROLLBACK {DELETE | PRESERVE} ROWS ] Specifies whether operations for the table are logged. The default is `NOT LOGGED ON ROLLBACK DELETE ROWS`. * NOT LOGGED: Specifies that insert, update, or delete operations against the table are not to be logged, but that the creation or dropping of the table is to be logged. During a ROLLBACK or ROLLBACK TO SAVEPOINT operation: - If the table had been created within a transaction, the table is dropped - If the table had been dropped within a transaction, the table is recreated, but without any data * ON ROLLBACK: Specifies the action that is to be taken on the not logged created temporary table when a ROLLBACK or ROLLBACK TO SAVEPOINT operation is performed. The default is DELETE ROWS. - DELETE ROWS: if the table data has been changed, all the rows will be deleted. - PRESERVE ROWS: rows of the table will be preserved. * LOGGED: specifies that insert, update, or delete operations against the table as well as the creation or dropping of the table are to be logged. With PostgreSQL only `NOT LOGGED ON ROLLBACK DELETE ROWS` can be supported. Creation or dropping of the Global Temporary Table are logged, see below "How the extension really works" for the details. ### [Installation](#installation) To install the pgtt extension you need at least a PostgreSQL version 12. Untar the pgtt tarball anywhere you want then you'll need to compile it with pgxs. The `pg_config` tool must be in your path. Depending on your installation, you may need to install some devel package. Once `pg_config` is in your path, do make sudo make install Then it will be possible to use it using `session_preload_libraries = 'pgtt'` in postgresql.conf To create and manage GTT using a non-superuser role you will have to grant the CREATE privilege on the `pgtt_schema` schema to the user. For example: GRANT ALL ON SCHEMA pgtt_schema TO pgtt_user1; To run test execute the following command as superuser: make installcheck An additional standalone test is provided to test the use of the extension as non superuser. The test can be executed using: mkdir results createdb gtt_privilege LANG=C psql -d gtt_privilege -f test/privilege.sql > results/privilege.out 2>&1 diff results/privilege.out test/expected/privilege.out dropdb gtt_privilege dropuser pgtt_user1 ### [Configuration](#configuration) - *pgtt.enabled* The extension can be enable / disable using this GUC, default is enabled. To disable the extension use: SET pgtt.enabled TO off; You can disable or enable the extension at any moment in a session. ### [Use of the extension](#use-of-the-extension) In all database where you want to use Global Temporary Tables you will have to create the extension using: CREATE EXTENSION pgtt; You can load the extension by setting in postgresql.conf : session_preload_libraries = 'pgtt'; or by setting it at database level as follow: DO $$ BEGIN EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''pgtt''', current_database()); END $$; The pgtt extension use a dedicated schema to store related objects, by default: `pgtt_schema`. The extension take care that this schema is always at end of the `search_path`. gtt_testdb=# SHOW search_path; search_path -------------------- public,pgtt_schema (1 row) gtt_testdb=# SET search_path TO appschema,public; SET gtt_testdb=# SHOW search_path; search_path -------------------------------- appschema, public, pgtt_schema (1 row) The pgtt schema is automatically added to the search_path when you load the extension and if you change the `search_path` later. You must also give the USAGE privilege on this schema to users that will manipulate the global temporary tables. #### Create a Global Temporary Table To create a GTT table named "test_table" use the following statement: CREATE GLOBAL TEMPORARY TABLE test_gtt_table ( id integer, lbl text ) ON COMMIT { PRESERVE | DELETE } ROWS; The GLOBAL keyword is obsolete but can be used safely, the only thing is that it will generate a warning: WARNING: GLOBAL is deprecated in temporary table creation If you don't want to be annoyed by this warning message you can use it like a comment instead: CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table ( LIKE other_table LIKE INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES ) ON COMMIT { PRESERVE | DELETE } ROWS; the extension will detect the GLOBAL keyword. As you can see in the example above the LIKE clause is supported, as well as the AS clause WITH DATA or WITH NO DATA (default): CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table AS SELECT * FROM source_table WITH DATA; In case of WITH DATA, the extension will fill the GTT with data returned from the SELECT statement for the current session only. PostgreSQL temporary table clause `ON COMMIT DROP` is not supported by the extension, GTT are persistent over transactions. If the clause is used an error will be raised. Temporary table rows are deleted or preserved at transactions commit following the clause: ON COMMIT { PRESERVE | DELETE } ROWS #### Drop a Global Temporary Table To drop a Global Temporary Table you just proceed as for a normal table: DROP TABLE test_gtt_table; A Global Temporary Table can be dropped even if it is used by other session. #### Create index on Global Temporary Table You can create indexes on the global temporary table: CREATE INDEX ON test_gtt_table (id); just like with any other tables. #### Constraints on Global Temporary Table You can add any constraint on a Global Temporary Table except FOREIGN KEYS. CREATE GLOBAL TEMPORARY TABLE t2 ( c1 serial PRIMARY KEY, c2 VARCHAR (50) UNIQUE NOT NULL, c3 boolean DEFAULT false ) The use of FOREIGN KEYS in a Global Temporary Table is not allowed. CREATE GLOBAL TEMPORARY TABLE t1 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); ERROR: attempt to create referential integrity constraint on global temporary table ALTER TABLE t2 ADD FOREIGN KEY (c1) REFERENCES source (id); ERROR: attempt to create referential integrity constraint on global temporary table Even if PostgreSQL allow foreign keys on temporary table, the pgtt extension try to mimic as much as possible the same behavior of Oracle and other RDBMS like DB2, SQL Server or MySQL. ORA-14455: attempt to create referential integrity constraint on temporary table. #### Partitioning Partitioning on Global Temporary Table is not supported, again not because PostgreSQL do not allow partition on temporary table but because other RDBMS like Oracle, DB2 and MySQL do not support it. SQL Server supports partition on global temporary table. ### [How the extension really works](#how-the-extension-really-works) #### Global Temporary Table usage When `pgtt.enabled` is true (default) and the extension have been loaded using any of the three methods: - `session_preload_libraries = 'pgtt'` in postgresql.conf - `ALTER DATABASE mydb SET session_preload_libraries = 'pgtt'` - in a session `LOAD 'pgtt';` the first access to the table using a SELECT, UPDATE or DELETE statement will produce the creation of a temporary table using the definition of the "template" table created during the call to `CREATE GLOBAL TEMPORARY TABLE` statement. Once the temporary table is created at the first access, the original SELECT, UPDATE or DELETE statement is automatically rerouted to the new regular temporary table. All other access will use the new temporary table, the `pg_temp*` schema where the table is created is always looked first in the search path this is why the "template" table is not concerned by subsequent access. Creating, renaming and removing a GTT is an administration task it shall not be done in an application session. Note that rerouting is active even if you add a namespace qualifier to the table. For example looking at the internal unlogged template table: bench=# LOAD 'pgtt'; LOAD bench=# CREATE /*GLOBAL*/ TEMPORARY TABLE test_tt (id int, lbl text) ON COMMIT PRESERVE ROWS; CREATE TABLE bench=# INSERT INTO test_tt VALUES (1, 'one'), (2, 'two'), (3, 'three'); INSERT 0 3 bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) will actually result in the same as looking at the associated temporary table like follow: bench=# SELECT * FROM test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) or bench=# SELECT * FROM pg_temp.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) If you want to really look at the template table to be sure that it contains no rows, you must disable the extension rerouting: bench=# SET pgtt.enabled TO off; SET bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+----- (0 rows) bench=# SET pgtt.enabled TO on; SET bench=# SELECT * FROM pgtt_schema.test_tt; id | lbl ----+------- 1 | one 2 | two 3 | three (3 rows) Look at test file for more examples. This also mean that you can relocate the extension in a dedicated namespace. This can be useful if your application's queries use the schema qualifier with the table name to access to the GTT and you can't change it. See t/sql/relocation.sql for an example. By default the extension is not relocatable in an other schema, there is some configuration change to perform to be able to use this feature. If you use the CREATE AS form with the WITH DATA clause like in this example: CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt_table AS SELECT * FROM source_table WITH DATA; the extension will first create the template unlogged table and will create immediately the associated temporary table filled with all data returned by the SELECT statement. The first access will not have to create the table it already exists with data. #### Table creation The extension intercept the call to `CREATE TEMPORARY TABLE ...` statement and look if there is the keyword `GLOBAL` or the comment `/*GLOBAL*/`. When it is found, instead of creating the temporary table, it creates a "template" unlogged persistent table following the temporary table definition. When the template is created it registers the table into a "catalog" table `pg_global_temp_tables`. Both objects are created in the extension schema `pgtt_schema`. When `pgtt.enabled` is false nothing is done. Here is the description of the catalog table: ``` Table « pgtt_schema.pg_global_temp_tables » Colonne | Type | Collationnement | NULL-able | Par défaut -----------+---------+-----------------+-----------+------------ relid | integer | | not null | nspname | name | | not null | relname | name | | not null | preserved | boolean | | | code | text | | | Index : "pg_global_temp_tables_nspname_relname_key" UNIQUE CONSTRAINT, btree (nspname, relname) ``` * `relid`: Oid of the "template" unlogged table. * `nspname`: namespace of the extension `pgtt_schema` by default. * `relname`: name of the GTT relation. * `preserved`: true or false for `ON COMMIT { PRESERVE | DELETE}`. * `code`: code used at Global Temporary Table creation time. #### Table removing The extension intercept the call to `DROP TABLE` and look in the `pg_global_temp_tables` table to see if it is declared. When it is found it drops the template unlogged table and the corresponding entry from the pgtt catalog table `pg_global_temp_tables`. When `pgtt.enabled` is false nothing is done. Dropping a GTT that is in use, when the temporary table has already been created, will raise an error. This is not allowed. #### Table renaming The extension intercept the call to `ALTER TABLE ... RENAME` and look in the `pg_global_temp_tables` table to see if it is declared. When it is found it renames the "template" table and update the name of the relation in the `pg_global_temp_tables` table. If the GTT has already been used in the session the corresponding temporary table exists, in this case the extension will refuse to rename it. It must be inactive to be renamed. When `pgtt.enabled` is false nothing is done. Renaming a GTT that is in use, when the temporary table has already been created, will raise an error. This is not allowed. #### pg_dump / pg_restore When dumping a database using the pgtt extension, the content of the "catalog" table `pg_global_temp_tables` will be dumped as well as all template unlogged tables. Restoring the dump will recreate the database in the same state. ### [Performances](#performances) Overhead of loading the extension but without using it in a pgbench tpcb-like scenario. * Without loading the extension ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_noload.sql starting vacuum...end. transaction type: test/bench/bench_noload.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 51741 latency average = 23.201 ms tps = 862.038042 (including connections establishing) tps = 862.165341 (excluding connections establishing) ``` * With loading the extension ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_load.sql starting vacuum...end. transaction type: test/bench/bench_load.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 51171 latency average = 23.461 ms tps = 852.495877 (including connections establishing) tps = 852.599010 (excluding connections establishing) ``` Now a test between using a regular temporary table and a PGTT in the pgbench tpcb-like scenario. * Using a regular Temporary Table ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_use_rtt.sql starting vacuum...end. transaction type: test/bench/bench_use_rtt.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 17153 latency average = 70.058 ms tps = 285.477860 (including connections establishing) tps = 285.514186 (excluding connections establishing) ``` * Using a Global Temporary Table Created using: CREATE GLOBAL TEMPORARY TABLE test_tt (id int, lbl text) ON COMMIT DELETE ROWS; ``` $ pgbench -h localhost bench -c 20 -j 4 -T 60 -f test/bench/bench_use_gtt.sql starting vacuum...end. transaction type: test/bench/bench_use_gtt.sql scaling factor: 1 query mode: simple number of clients: 20 number of threads: 4 duration: 60 s number of transactions actually processed: 17540 latency average = 68.495 ms tps = 291.993502 (including connections establishing) tps = 292.028832 (excluding connections establishing) ``` Even if this last test shows a significant performances improvement comparing to regular temporary tables, most of the time this will not be the case. ### [Authors](#authors) - Gilles Darold - Julien Rouhaud ### [License](#license) This extension is free software distributed under the PostgreSQL Licence. Copyright (c) 2018-2026, Gilles Darold darold-pgtt-fe8b2d5/sql/000077500000000000000000000000001522714605300152405ustar00rootroot00000000000000darold-pgtt-fe8b2d5/sql/pgtt--2.0.0.sql000066400000000000000000000014701522714605300174510ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.1.0.sql000066400000000000000000000014701522714605300174520ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.10.0.sql000066400000000000000000000014321522714605300175300ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.2.0.sql000066400000000000000000000014701522714605300174530ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.3.0.sql000066400000000000000000000014701522714605300174540ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.4.0.sql000066400000000000000000000014701522714605300174550ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.5.0.sql000066400000000000000000000014701522714605300174560ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.6.0.sql000066400000000000000000000014701522714605300174570ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.7.0.sql000066400000000000000000000014701522714605300174600ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.8.0.sql000066400000000000000000000014701522714605300174610ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Create schema dedicated to the global temporary table ---- CREATE SCHEMA IF NOT EXISTS @extschema@; REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--2.9.0.sql000066400000000000000000000014321522714605300174600ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--3.0.0.sql000066400000000000000000000014321522714605300174500ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--3.1.0.sql000066400000000000000000000014321522714605300174510ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--3.2.0.sql000066400000000000000000000014321522714605300174520ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.0.0.sql000066400000000000000000000014321522714605300174510ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.1.0.sql000066400000000000000000000014321522714605300174520ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.2.0.sql000066400000000000000000000014321522714605300174530ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.3.0.sql000066400000000000000000000014321522714605300174540ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.4.0.sql000066400000000000000000000014321522714605300174550ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.5.0.sql000066400000000000000000000014321522714605300174560ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); GRANT ALL ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/sql/pgtt--4.6.0.sql000066400000000000000000000034731522714605300174660ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pgtt" to load this file. \quit ---- -- Fix privileges on schema dedicated to the global temporary table ---- REVOKE ALL ON SCHEMA @extschema@ FROM PUBLIC; GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC; ---- -- Table used to store information about Global Temporary Tables. -- Content will be loaded in memory by the pgtt extension. ---- CREATE TABLE @extschema@.pg_global_temp_tables ( relid integer NOT NULL, nspname name NOT NULL, relname name NOT NULL, preserved boolean, code text, UNIQUE (nspname, relname) ); ---- -- SECURITY (fix for public write access to the catalog table): -- Every session that uses pgtt needs to be able to *read* this table -- (gtt_load_global_temporary_tables() scans it on first use in every -- backend, and it is documented as an introspectable catalog), so -- SELECT is kept available to PUBLIC. INSERT/UPDATE/DELETE/TRUNCATE -- are intentionally NOT granted to PUBLIC any more: previously ALL -- privileges were granted here, which let any authenticated role -- directly tamper with (or delete) any other role's GTT registration -- with no ownership check at all, entirely bypassing the ownership -- checks the extension's CREATE/DROP/RENAME TABLE interception -- performs. Roles that need to create/rename/drop GTTs (i.e. roles -- the DBA has granted CREATE on @extschema@ to, per the README) must -- now also be granted explicit write access on this table, e.g.: -- GRANT SELECT, INSERT, UPDATE, DELETE -- ON @extschema@.pg_global_temp_tables TO ; ---- REVOKE ALL ON TABLE @extschema@.pg_global_temp_tables FROM PUBLIC; GRANT SELECT ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC; -- Include tables into pg_dump SELECT pg_catalog.pg_extension_config_dump('pg_global_temp_tables', ''); darold-pgtt-fe8b2d5/test/000077500000000000000000000000001522714605300154205ustar00rootroot00000000000000darold-pgtt-fe8b2d5/test/bench/000077500000000000000000000000001522714605300164775ustar00rootroot00000000000000darold-pgtt-fe8b2d5/test/bench/bench_load.sql000066400000000000000000000010511522714605300212730ustar00rootroot00000000000000\set aid random(1, 100000 * :scale) \set bid random(1, 1 * :scale) \set tid random(1, 10 * :scale) \set delta random(-5000, 5000) -- LOAD 'pgtt'; BEGIN; UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); END; darold-pgtt-fe8b2d5/test/bench/bench_use_gtt.sql000066400000000000000000000013361522714605300220340ustar00rootroot00000000000000\set aid random(1, 100000 * :scale) \set bid random(1, 1 * :scale) \set tid random(1, 10 * :scale) \set delta random(-5000, 5000) -- LOAD 'pgtt'; BEGIN; INSERT INTO test_tt (id, lbl) SELECT i, md5(i::text) FROM generate_series(1, 1000) i; UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; UPDATE test_tt SET id = id+100 WHERE id > 500; INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); SELECT * FROM test_tt ORDER BY id DESC LIMIT 10; END; darold-pgtt-fe8b2d5/test/bench/bench_use_rtt.sql000066400000000000000000000015201522714605300220420ustar00rootroot00000000000000\set aid random(1, 100000 * :scale) \set bid random(1, 1 * :scale) \set tid random(1, 10 * :scale) \set delta random(-5000, 5000) SET client_min_messages TO error; CREATE TEMPORARY TABLE IF NOT EXISTS test_tt (id integer, lbl varchar) ON COMMIT DELETE ROWS; BEGIN; INSERT INTO test_tt (id, lbl) SELECT i, md5(i::text) FROM generate_series(1, 1000) i; UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid; UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid; UPDATE test_tt SET id = id+100 WHERE id > 500; INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP); SELECT * FROM test_tt ORDER BY id DESC LIMIT 10; END; darold-pgtt-fe8b2d5/test/expected/000077500000000000000000000000001522714605300172215ustar00rootroot00000000000000darold-pgtt-fe8b2d5/test/expected/.15_security_grants.out.swp000066400000000000000000000400001522714605300243640ustar00rootroot00000000000000b0VIM 9.1@‡[j¦�bI,gillesthinkbook/home/git/pgtt/test/expected/15_security_grants.out 3210#"! UtpYZad�Yû¿¼?¿…?<7ñ ° “ Q Ç ª | $ Þ Æ § { W +  ÷ Ü Á ¸ · { l  × ¶ «   – Ž � H   ˜j<õȽ²©¨�L+¿…!ç”Z.ôÛ‹U乃[% Êœ�laVMLݯ�€-------------+-------------------+----------- nspname | relname | preserved SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants';(0 rows)----+----- id | lbl SELECT * FROM t_security_grants;\c - gtt_owner-- unaffected by the rejected attempts above.-- The registration and the data gtt_owner owns must be completelySET pgtt.enabled TO on;ERROR: permission denied for table t_security_grantsTRUNCATE pgtt_schema.t_security_grants;ERROR: permission denied for table t_security_grantsDELETE FROM pgtt_schema.t_security_grants;ERROR: permission denied for table t_security_grantsUPDATE pgtt_schema.t_security_grants SET lbl = 'hijacked';ERROR: permission denied for table t_security_grantsINSERT INTO pgtt_schema.t_security_grants VALUES (99, 'should not be allowed');SET pgtt.enabled TO off;ERROR: permission denied for table pg_global_temp_tablesTRUNCATE pgtt_schema.pg_global_temp_tables;ERROR: permission denied for table pg_global_temp_tablesDELETE FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants';ERROR: permission denied for table pg_global_temp_tablesUPDATE pgtt_schema.pg_global_temp_tables SET code = 'hijacked' WHERE relname = 't_security_grants';ERROR: permission denied for table pg_global_temp_tablesINSERT INTO pgtt_schema.pg_global_temp_tables VALUES (0, 'pgtt_schema', 'spoofed_gtt', true, 'id integer');-- fail with a permission error.-- ...but can no longer write to either. Every statement below mustSET pgtt.enabled TO on;(0 rows)----+----- id | lbl SELECT * FROM pgtt_schema.t_security_grants;SET pgtt.enabled TO off;(1 row) pgtt_schema | t_security_grants | t-------------+-------------------+----------- nspname | relname | preserved SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants';\c - gtt_other-- the catalog and the template table directly...-- gtt_other (no special grants, PUBLIC-equivalent) can still SELECT(1 row) 1 | one----+----- id | lbl SELECT * FROM t_security_grants;INSERT INTO t_security_grants VALUES (1, 'one');CREATE /*GLOBAL*/ TEMPORARY TABLE t_security_grants (id integer, lbl text) ON COMMIT PRESERVE ROWS;\c - gtt_owner-- gtt_owner creates a GTT using its documented privileges.(0 rows)---------+---------------- grantee | privilege_type ORDER BY privilege_type; AND grantee = 'public' AND table_name = 'pg_global_temp_tables' WHERE table_schema = 'pgtt_schema' FROM information_schema.role_table_grantsSELECT grantee, privilege_type-- PUBLIC now, not ALL.-- Baseline (superuser) sanity check: the table only grants SELECT toGRANT SELECT, INSERT, UPDATE, DELETE ON pgtt_schema.pg_global_temp_tables TO gtt_owner;GRANT ALL ON SCHEMA pgtt_schema TO gtt_owner;CREATE ROLE gtt_owner LOGIN;-- fix now requires (documented in the 4.5.0->4.5.1 upgrade script).-- instructions describe, plus the additional table-level grant this-- A role provisioned exactly as the README's non-superuser setupCREATE ROLE gtt_other LOGIN;-- USAGE grant to PUBLIC (i.e. representative of PUBLIC itself).-- A role with no special pgtt privileges beyond the schema's default-------- pg_global_temp_tables) can still fully create, use and drop a GTT.-- the additional explicit grant this fix now requires on-- documented non-superuser setup (CREATE on the pgtt schema, plus-- table, only SELECT -- and that a role provisioned per the-- pg_global_temp_tables catalog table or to a GTT's "template"-- Test that PUBLIC no longer has write access to the shared---- Regression test to Global Temporary Table implementation----ad®ÒÛÓÒÑ(1 row) pgtt_schema | t_security_grants | tdarold-pgtt-fe8b2d5/test/expected/00_init.out000066400000000000000000000011721522714605300212150ustar00rootroot00000000000000/* * Must be executed before all regression test. */ -- Create the PostgreSQL extension CREATE EXTENSION pgtt; -- Set session_preload_libraries DO $$ BEGIN EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''pgtt''', current_database()); END $$; -- Create a regular table with some rows CREATE TABLE source (id serial PRIMARY KEY, c2 varchar(50) UNIQUE NOT NULL, lbl varchar DEFAULT '-'); COMMENT ON TABLE source IS 'Table used to demonstrate GTT create as feature'; COMMENT ON COLUMN source.id IS 'auto generated column'; CREATE INDEX ON source(lbl); INSERT INTO source VALUES (1,'one'), (2,'two'),(3,'three'); darold-pgtt-fe8b2d5/test/expected/01_oncommitdelete.out000066400000000000000000000066651522714605300232770ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with ON COMMIT DELETE ROWS clause. -- -- Test drop of GTT when in use throwing error and effective -- when done in a separate session. -- ---- -- Create a GTT like table to test ON COMMIT DELETE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT DELETE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer,... ^ -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | f | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Second insert, the temporary table exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) COMMIT; -- No row must perstist after the commit SELECT * FROM t_glob_temptable1; id | lbl ----+----- (0 rows) -- With holdabe cursor the rows must remain BEGIN; INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); DECLARE curs1 CURSOR WITH HOLD FOR SELECT * FROM t_glob_temptable1; FETCH curs1; id | lbl ----+----- 1 | One (1 row) COMMIT; -- No row must perstist after the commit for the cursor FETCH curs1; id | lbl ----+----- 2 | Two (1 row) -- But not for a new select SELECT * FROM t_glob_temptable1; id | lbl ----+----- (0 rows) CLOSE curs1; -- Drop the global temporary table: fail because it is in use DROP TABLE t_glob_temptable1; ERROR: can not drop a GTT that is in use. -- Reconnect and drop it \c - - SHOW search_path; search_path ------------------------------ "$user", public, pgtt_schema (1 row) DROP TABLE t_glob_temptable1; VACUUM pg_class; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- should be empty nspname | relname | preserved | code ---------+---------+-----------+------ (0 rows) -- The "template" unlogged table should not exists anymore SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname ---------+--------- (0 rows) darold-pgtt-fe8b2d5/test/expected/02_oncommitpreserve.out000066400000000000000000000043541522714605300236620ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with ON COMMIT PRESERVE ROWS clause. -- -- Test rereouting of truncate on the temporary table. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer,... ^ -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Second insert, the temporary table exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) COMMIT; SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- Truncate GTT TRUNCATE t_glob_temptable1; SELECT * FROM t_glob_temptable1; id | lbl ----+----- (0 rows) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/03_createontruncate.out000066400000000000000000000056011522714605300236240ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for TRUNCATE on GTT with temporary table first access in a transaction. -- The temporary table must not persist after the transaction rollback. -- ---- -- Create a GTT like table CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer,... ^ -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) BEGIN; -- Truncate first will not create the temporary table, it will -- be operated on the "template" table which will do nothing. TRUNCATE t_glob_temptable1; -- Look if we have two tables now SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- Now truncate the temporary table TRUNCATE t_glob_temptable1; -- Verify that there is no mo rows SELECT * FROM t_glob_temptable1; id | lbl ----+----- (0 rows) ROLLBACK; -- The "template" unlogged table exists, but the -- temporary table not because of the rollback SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; -- Reconnect to test locking, see #41 \c - - -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt (id int, lbl text); SELECT * FROM pgtt_schema.test_gtt; id | lbl ----+----- (0 rows) SELECT * FROM pgtt_schema.test_gtt; -- success id | lbl ----+----- (0 rows) \c - - -- Cleanup DROP TABLE test_gtt; darold-pgtt-fe8b2d5/test/expected/04_rename.out000066400000000000000000000160141522714605300215260ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on renaming GTT using ALTER TABLE ... RENAME TO ... -- -- Renaming a GGT when the temporary table has already been -- created is not allowed and must be done in a new session. -- ---- -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; CREATE INDEX ON pgtt_schema.t_glob_temptable1 (id); CREATE INDEX ON pgtt_schema.t_glob_temptable1 (lbl); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-------------+-----------+------------+----------------- id | integer | | f | lbl | text | | f | (2 rows) -- With indexes defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | indisprimary | indisunique | pg_get_indexdef | pg_get_constraintdef | contype ---------------------------+--------------+-------------+-------------------------------------------------------------------------------+----------------------+--------- t_glob_temptable1_id_idx | f | f | CREATE INDEX t_glob_temptable1_id_idx ON t_glob_temptable1 USING btree (id) | | t_glob_temptable1_lbl_idx | f | f | CREATE INDEX t_glob_temptable1_lbl_idx ON t_glob_temptable1 USING btree (lbl) | | (2 rows) -- Rename the table ALTER TABLE t_glob_temptable1 RENAME TO t_glob_temptable2; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable2 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable2' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-------------+-----------+------------+----------------- id | integer | | f | lbl | text | | f | (2 rows) -- With indexes still defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable2' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | indisprimary | indisunique | pg_get_indexdef | pg_get_constraintdef | contype ---------------------------+--------------+-------------+-------------------------------------------------------------------------------+----------------------+--------- t_glob_temptable1_id_idx | f | f | CREATE INDEX t_glob_temptable1_id_idx ON t_glob_temptable2 USING btree (id) | | t_glob_temptable1_lbl_idx | f | f | CREATE INDEX t_glob_temptable1_lbl_idx ON t_glob_temptable2 USING btree (lbl) | | (2 rows) -- With the first insert a temporary table is created and the row inseterd SET pgtt.enabled TO on; INSERT INTO t_glob_temptable2 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable2'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable2 pg_temp_x | t_glob_temptable2 (2 rows) -- The table doesn't exist anymore SET pgtt.enabled TO off; \d t_glob_temptable1; SET pgtt.enabled TO on; INSERT INTO t_glob_temptable2 VALUES (2, 'two'); SELECT * FROM t_glob_temptable2 WHERE id = 2; id | lbl ----+----- 2 | two (1 row) -- Rename the table when the temporary table has already been created is not allowed ALTER TABLE t_glob_temptable2 RENAME TO t_glob_temptable1; ERROR: a temporary table has been created and is active, can not rename the GTT table in this session. \c - - ALTER TABLE t_glob_temptable2 RENAME TO t_glob_temptable1; -- Look if the renaming is effective SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/05_useindex.out000066400000000000000000000151071522714605300221060ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with index, the temporary table must have the -- index too. -- ---- -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; SET pgtt.enabled TO off; CREATE INDEX ON pgtt_schema.t_glob_temptable1 (id); CREATE INDEX ON pgtt_schema.t_glob_temptable1 (lbl); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-------------+-----------+------------+----------------- id | integer | | f | lbl | text | | f | (2 rows) -- With indexes still defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | indisprimary | indisunique | pg_get_indexdef | pg_get_constraintdef | contype ---------------------------+--------------+-------------+-------------------------------------------------------------------------------+----------------------+--------- t_glob_temptable1_id_idx | f | f | CREATE INDEX t_glob_temptable1_id_idx ON t_glob_temptable1 USING btree (id) | | t_glob_temptable1_lbl_idx | f | f | CREATE INDEX t_glob_temptable1_lbl_idx ON t_glob_temptable1 USING btree (lbl) | | (2 rows) SET pgtt.enabled TO on; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- and that the temporary table has the indexes too SELECT tablename, indexname FROM pg_indexes WHERE tablename = 't_glob_temptable1' and schemaname LIKE 'pg_temp_%' ORDER BY tablename, indexname; tablename | indexname -------------------+--------------------------- t_glob_temptable1 | t_glob_temptable1_id_idx t_glob_temptable1 | t_glob_temptable1_lbl_idx (2 rows) INSERT INTO t_glob_temptable1 VALUES (2, 'two'); -- Verify that the index is used SET enable_bitmapscan TO off; EXPLAIN (COSTS OFF) SELECT * FROM t_glob_temptable1 WHERE id = 2; QUERY PLAN ---------------------------------------------------------------- Index Scan using t_glob_temptable1_id_idx on t_glob_temptable1 Index Cond: (id = 2) (2 rows) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable2 (id integer, lbl text) ON COMMIT PRESERVE ROWS; CREATE INDEX ON t_glob_temptable2 (id); SELECT * FROM t_glob_temptable2; id | lbl ----+----- (0 rows) -- Must complain that the GTT is in use CREATE INDEX ON t_glob_temptable2 (lbl); ERROR: a temporary table has been created and is active, can not add an index on the GTT table in this session. SELECT pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = 'pgtt_schema.t_glob_temptable2'::regclass::oid AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; pg_get_indexdef ----------------------------------------------------------------------------------------- CREATE INDEX t_glob_temptable2_id_idx ON pgtt_schema.t_glob_temptable2 USING btree (id) (1 row) -- Check that we do not break LIKE ... USING INDEXES CREATE TABLE tb_with_index (id integer PRIMARY KEY, lbl varchar); CREATE INDEX ON tb_with_index(lbl); CREATE TEMPORARY TABLE temptb_with_index (LIKE tb_with_index INCLUDING INDEXES); SELECT c2.relname, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true) FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = 'temptb_with_index'::regclass AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | pg_get_indexdef | pg_get_constraintdef ---------------------------+----------------------------------------------------------------------------------+---------------------- temptb_with_index_pkey | CREATE UNIQUE INDEX temptb_with_index_pkey ON temptb_with_index USING btree (id) | PRIMARY KEY (id) temptb_with_index_lbl_idx | CREATE INDEX temptb_with_index_lbl_idx ON temptb_with_index USING btree (lbl) | (2 rows) \c - - DROP TABLE t_glob_temptable2; darold-pgtt-fe8b2d5/test/expected/06_createas.out000066400000000000000000000046061522714605300220540ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with TABLE ... AS clause. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 AS SELECT * FROM source WITH DATA; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 AS SELECT * ... ^ -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+--------------------------- pgtt_schema | t_glob_temptable1 | t | AS SELECT * FROM source (1 row) -- A "template" unlogged table should exists as well as -- the temporary table as we have used WITH DATA SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | c2 | lbl ----+-------+----- 1 | one | - 2 | two | - 3 | three | - (3 rows) SET pgtt.enabled TO on; -- Look at the temporary table itself, it must have the rows SELECT * FROM t_glob_temptable1 ORDER BY id; id | c2 | lbl ----+-------+----- 1 | one | - 2 | two | - 3 | three | - (3 rows) BEGIN; -- With a new row INSERT INTO t_glob_temptable1 VALUES (4, 'four'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | c2 | lbl ----+-------+----- 1 | one | - 2 | two | - 3 | three | - (3 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1 ORDER BY id; id | c2 | lbl ----+-------+----- 1 | one | - 2 | two | - 3 | three | - 4 | four | (4 rows) COMMIT; SELECT * FROM t_glob_temptable1 ORDER BY id; id | c2 | lbl ----+-------+----- 1 | one | - 2 | two | - 3 | three | - 4 | four | (4 rows) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/07_createlike.out000066400000000000000000000167031522714605300223770ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with TABLE ... (LIKE ...) clause. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 ( LIKE source INCLUDING ALL ) ON COMMIT PRESERVE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 ( ^ -- Look at table description SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-----------------------+------------------------------------+------------+----------------------- id | integer | nextval('source_id_seq'::regclass) | t | auto generated column c2 | character varying(50) | | t | lbl | character varying | '-'::character varying | f | (3 rows) -- With indexes defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | indisprimary | indisunique | pg_get_indexdef | pg_get_constraintdef | contype ---------------------------+--------------+-------------+------------------------------------------------------------------------------------+----------------------+--------- t_glob_temptable1_pkey | t | t | CREATE UNIQUE INDEX t_glob_temptable1_pkey ON t_glob_temptable1 USING btree (id) | PRIMARY KEY (id) | p t_glob_temptable1_c2_key | f | t | CREATE UNIQUE INDEX t_glob_temptable1_c2_key ON t_glob_temptable1 USING btree (c2) | UNIQUE (c2) | u t_glob_temptable1_lbl_idx | f | f | CREATE INDEX t_glob_temptable1_lbl_idx ON t_glob_temptable1 USING btree (lbl) | | (3 rows) -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+----------------------- pgtt_schema | t_glob_temptable1 | t | + | | | LIKE source + | | | INCLUDING ALL+ | | | (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look at temp table description SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg_temp_%' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-----------------------+------------------------------------+------------+----------------------- id | integer | nextval('source_id_seq'::regclass) | t | auto generated column c2 | character varying(50) | | t | lbl | character varying | '-'::character varying | f | (3 rows) SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg_temp_%' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; relname | indisprimary | indisunique | pg_get_indexdef | pg_get_constraintdef | contype ---------------------------+--------------+-------------+------------------------------------------------------------------------------------+----------------------+--------- t_glob_temptable1_pkey | t | t | CREATE UNIQUE INDEX t_glob_temptable1_pkey ON t_glob_temptable1 USING btree (id) | PRIMARY KEY (id) | p t_glob_temptable1_c2_key | f | t | CREATE UNIQUE INDEX t_glob_temptable1_c2_key ON t_glob_temptable1 USING btree (c2) | UNIQUE (c2) | u t_glob_temptable1_lbl_idx | f | f | CREATE INDEX t_glob_temptable1_lbl_idx ON t_glob_temptable1 USING btree (lbl) | | (3 rows) -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | c2 | lbl ----+----+----- (0 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1 ORDER BY id; id | c2 | lbl ----+-----+----- 1 | One | - (1 row) COMMIT; SELECT * FROM t_glob_temptable1 ORDER BY id; id | c2 | lbl ----+-----+----- 1 | One | - (1 row) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/08_plplgsql.out000066400000000000000000000122001522714605300221120ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT defined inside a PLPGSQL function. -- ---- CREATE OR REPLACE FUNCTION test_temp_table () RETURNS integer AS $$ DECLARE nrows integer; BEGIN CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1(id int, lbl text) ON COMMIT PRESERVE ROWS; INSERT INTO t_glob_temptable1 (id, lbl) SELECT i, md5(i::text) FROM generate_series(1, 10) i; SELECT count(*) INTO nrows FROM t_glob_temptable1 ; RETURN nrows; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Look at Global Temporary Table definition: none SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code ---------+---------+-----------+------ (0 rows) -- Call the function, must returns 10 rows SELECT test_temp_table(); test_temp_table ----------------- 10 (1 row) -- Look at Global Temporary Table definition: table exists SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+------------------ pgtt_schema | t_glob_temptable1 | t | id int, lbl text (1 row) -- Look if the temporary table exists outside the function call SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- A "template" unlogged table should exists SET pgtt.enabled TO off; SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-------------+-----------+------------+----------------- id | integer | | f | lbl | text | | f | (2 rows) -- Get rows from the template table SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) -- Get rows from the temporary table SET pgtt.enabled TO on; SELECT * FROM t_glob_temptable1; id | lbl ----+---------------------------------- 1 | c4ca4238a0b923820dcc509a6f75849b 2 | c81e728d9d4c2f636f067f89cc14862c 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 4 | a87ff679a2f3e71d9181a67b7542122c 5 | e4da3b7fbbce2345d7772b0674a318d5 6 | 1679091c5a880faf6fb5e6087eb1b2dc 7 | 8f14e45fceea167a5a36dedd4bea2543 8 | c9f0f895fb98ab9159f51fd0297e236d 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 10 | d3d9446802a44259755d38e6d163e820 (10 rows) -- Reconnect without dropping the global temporary table \c - - SET pgtt.enabled TO off; VACUUM pg_class; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- Verify that only the temporary table have been dropped -- Only the "template" unlogged table should exists SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Look at Global Temporary Table definition, the table must be present SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+------------------ pgtt_schema | t_glob_temptable1 | t | id int, lbl text (1 row) SET pgtt.enabled TO on; -- Call the function a second time - must fail the table already exists SELECT test_temp_table(); ERROR: relation "t_glob_temptable1" already exists CONTEXT: SQL statement "CREATE UNLOGGED TABLE pgtt_schema.t_glob_temptable1 (id int, lbl text)" SQL statement "CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1(id int, lbl text) ON COMMIT PRESERVE ROWS" PL/pgSQL function test_temp_table() line 6 at SQL statement -- Look at temporary table content, must be empty after the reconnect and function failure SELECT * FROM t_glob_temptable1; id | lbl ----+----- (0 rows) -- Now the "template" unlogged table should exists as well as the temporary table SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; DROP FUNCTION test_temp_table(); darold-pgtt-fe8b2d5/test/expected/09_transaction.out000066400000000000000000000052011522714605300226050ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for transaction manamgement on GTT. -- -- Test that the creation a GTT in rollbacked transaction -- will not preserve it. -- ---- BEGIN; -- Register the Global temporary table in a transaction CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) ROLLBACK; -- The GTT must not exists SELECT * FROM t_glob_temptable1; ERROR: relation "t_glob_temptable1" does not exist LINE 1: SELECT * FROM t_glob_temptable1; ^ -- Return nothing SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname ---------+--------- (0 rows) -- Register the Global temporary table outside the transaction CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; BEGIN; -- Drop the GTT DROP TABLE t_glob_temptable1; ROLLBACK; -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- The GTT must not exists SELECT * FROM t_glob_temptable1; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- Both tables muste exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/10_foreignkey.out000066400000000000000000000034501522714605300224160ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for unsupported foreign keys on GTT. -- ---- -- Import the library -- LOAD 'pgtt'; -- Must throw ERROR: attempt to create referential integrity constraint on temporary table. CREATE /*GLOBAL*/ TEMPORARY TABLE t2 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); ERROR: attempt to create referential integrity constraint on global temporary table BEGIN; CREATE /*GLOBAL*/ TEMPORARY TABLE t2 (c1 integer); -- Must throw ERROR: attempt to create referential integrity constraint on temporary table. ALTER TABLE t2 ADD FOREIGN KEY (c1) REFERENCES source (id); ERROR: attempt to create referential integrity constraint on global temporary table ROLLBACK; BEGIN; -- Must be valid statement CREATE TABLE t3 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); ROLLBACK; ---- -- Regression test for the column-level REFERENCES form of a foreign -- key, which a previous text-regex implementation of this check -- (searching only for the literal substring "FOREIGN KEY") failed to -- catch, since this syntax contains no such substring. ---- -- Must throw the same error as the table-level FOREIGN KEY (...) form above. CREATE /*GLOBAL*/ TEMPORARY TABLE t4 (c1 integer REFERENCES source (id)); ERROR: attempt to create referential integrity constraint on global temporary table BEGIN; CREATE /*GLOBAL*/ TEMPORARY TABLE t4 (c1 integer); -- Column-level REFERENCES added later via ALTER TABLE must also fail, -- exactly like the table-level form tested above. ALTER TABLE t4 ADD FOREIGN KEY (c1) REFERENCES source (id); ERROR: attempt to create referential integrity constraint on global temporary table ROLLBACK; BEGIN; -- Must be valid statement: column-level REFERENCES on a regular table. CREATE TABLE t5 (c1 integer REFERENCES source (id)); ROLLBACK; darold-pgtt-fe8b2d5/test/expected/11_after_error.out000066400000000000000000000071541522714605300225740ustar00rootroot00000000000000-- Import the library -- LOAD 'pgtt'; -- Create a GTT like table to test ON COMMIT DELETE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT DELETE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer,... ^ -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | f | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Verify the registering of the temporary table SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | f | id integer, lbl text (1 row) -- Second insert failure INSERT INTO t_glob_temptable1 VALUES (2, two); ERROR: column "two" does not exist LINE 1: INSERT INTO t_glob_temptable1 VALUES (2, two); ^ ROLLBACK; -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 (1 row) -- Insert a new row BEGIN; -- With the first insert some value in the temporary table -- Should not return an error that table doesn't exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; regexp_replace | relname ----------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_x | t_glob_temptable1 (2 rows) -- Verify the insert SELECT * FROM t_glob_temptable1; id | lbl ----+----- 2 | Two (1 row) COMMIT; -- Reconnect and drop the GTT \c - - -- LOAD 'pgtt'; SHOW search_path; search_path ------------------------------ "$user", public, pgtt_schema (1 row) DROP TABLE t_glob_temptable1; VACUUM pg_class; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- should be empty nspname | relname | preserved | code ---------+---------+-----------+------ (0 rows) -- The "template" unlogged table should not exists anymore SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; nspname | relname ---------+--------- (0 rows) darold-pgtt-fe8b2d5/test/expected/12_droptable.out000066400000000000000000000032131522714605300222270ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on dropping regular table when the extension is loaded -- ---- -- Import the library -- LOAD 'pgtt'; -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; attname | format_type | substring | attnotnull | col_description ---------+-------------+-----------+------------+----------------- id | integer | | f | lbl | text | | f | (2 rows) -- Create and drop a regular table CREATE TABLE t1 (id integer); DROP TABLE t1; -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/13_searchpath.out000066400000000000000000000041431522714605300224010ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on search_path forced by the extension -- ---- DROP EXTENSION pgtt; SHOW search_path ; search_path ------------------------------ "$user", public, pgtt_schema (1 row) create schema first_schema; create schema second_schema; SET search_path TO "$user",public,pg_catalog,fisrt_schema,second_schema; SHOW search_path ; search_path ----------------------------------------------------------------------- "$user", public, pg_catalog, fisrt_schema, second_schema, pgtt_schema (1 row) CREATE EXTENSION pgtt; SHOW search_path ; search_path ----------------------------------------------------------------------- "$user", public, pg_catalog, fisrt_schema, second_schema, pgtt_schema (1 row) SET search_path TO pg_catalog; SHOW search_path ; search_path ------------------------- pg_catalog, pgtt_schema (1 row) SET search_path TO public; SHOW search_path ; search_path --------------------- public, pgtt_schema (1 row) -- Test only pg_catalog in search_path. DROP EXTENSION pgtt; \c - - SHOW search_path; search_path ----------------- "$user", public (1 row) SET search_path TO pg_catalog; SHOW search_path; search_path ------------- pg_catalog (1 row) CREATE EXTENSION pgtt; SHOW search_path; search_path -------------------------- pgtt_schema, pg_catalog (1 row) -- Test the pg_catalog at end of search_path. DROP EXTENSION pgtt; \c - - SET search_path TO "$user", public, pg_catalog; SHOW search_path; search_path ----------------------------- "$user", public, pg_catalog (1 row) CREATE EXTENSION pgtt; SHOW search_path; search_path ------------------------------------------ "$user", public, pgtt_schema, pg_catalog (1 row) -- Test empty search path like set by pg_dump SELECT pg_catalog.set_config('search_path', '', false); set_config ------------ (1 row) SHOW search_path; search_path ------------- (1 row) darold-pgtt-fe8b2d5/test/expected/14_concurrent_index.out000066400000000000000000000046771522714605300236450ustar00rootroot00000000000000-- -- Test for CREATE INDEX CONCURRENTLY on pgtt -- -- Start with a clean slate to ensure consistent output SET client_min_messages TO WARNING; DROP EXTENSION IF EXISTS pgtt CASCADE; CREATE EXTENSION pgtt; RESET client_min_messages; -- 1. Create the Global Temporary Table CREATE GLOBAL TEMPORARY TABLE my_gtt_concurrent ( id int, lbl text ) ON COMMIT PRESERVE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE my_gtt_concurrent ( ^ -- 2. Activate the GTT -- We insert a row so that the local temporary table is physically created. -- This forces the namespace check in pgtt.c to see a "pg_temp" namespace. INSERT INTO my_gtt_concurrent VALUES (1, 'initial_data'); -- 3. Negative Test: Standard CREATE INDEX -- This should still FAIL because the GTT is active and we are not using CONCURRENTLY. -- The extension preserves the error here: if (!stmt->concurrent) ereport(ERROR... \set ON_ERROR_STOP 0 CREATE INDEX idx_gtt_fail ON my_gtt_concurrent (id); ERROR: a temporary table has been created and is active, can not add an index on the GTT table in this session. \set ON_ERROR_STOP 1 -- 4. Positive Test: CREATE INDEX CONCURRENTLY -- This should SUCCEED. The extension bypasses the error when stmt->concurrent is true -- and uses ShareUpdateExclusiveLock. CREATE INDEX CONCURRENTLY idx_gtt_success ON my_gtt_concurrent (id); -- Verify the index is actually there and valid SELECT CASE WHEN schemaname LIKE 'pg_temp_%' THEN 'pg_temp' ELSE schemaname END AS schemaname, tablename, indexname, regexp_replace( replace(indexdef, 'public.', 'pg_temp.'), 'pg_temp_\d+', 'pg_temp', 'g' ) as indexdef FROM pg_indexes WHERE tablename = 'my_gtt_concurrent' ORDER BY indexname; schemaname | tablename | indexname | indexdef ------------+-------------------+-----------------+---------------------------------------------------------------------------- pg_temp | my_gtt_concurrent | idx_gtt_success | CREATE INDEX idx_gtt_success ON pg_temp.my_gtt_concurrent USING btree (id) (1 row) -- 5. Test REINDEX CONCURRENTLY -- Assuming the implementation handles ReindexStmt similarly to IndexStmt, -- or it relies on standard PG behavior for the reindex once the index exists. REINDEX INDEX CONCURRENTLY idx_gtt_success; -- No cleanup needed: pg_regress will destroy the environment. darold-pgtt-fe8b2d5/test/expected/15_security_grants.out000066400000000000000000000075111522714605300235100ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that PUBLIC no longer has write access to the shared -- pg_global_temp_tables catalog table or to a GTT's "template" -- table, only SELECT -- and that a role provisioned per the -- documented non-superuser setup (CREATE on the pgtt schema, plus -- the additional explicit grant this fix now requires on -- pg_global_temp_tables) can still fully create, use and drop a GTT. -- ---- -- A role with no special pgtt privileges beyond the schema's default -- USAGE grant to PUBLIC (i.e. representative of PUBLIC itself). CREATE ROLE gtt_other LOGIN; -- A role provisioned exactly as the README's non-superuser setup -- instructions describe, plus the additional table-level grant this -- fix now requires (documented in the 4.5.0->4.5.1 upgrade script). CREATE ROLE gtt_owner LOGIN; GRANT ALL ON SCHEMA pgtt_schema TO gtt_owner; GRANT SELECT, INSERT, UPDATE, DELETE ON pgtt_schema.pg_global_temp_tables TO gtt_owner; -- Baseline (superuser) sanity check: the table only grants SELECT to -- PUBLIC now, not ALL. SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_schema = 'pgtt_schema' AND table_name = 'pg_global_temp_tables' AND grantee = 'public' ORDER BY privilege_type; grantee | privilege_type ---------+---------------- (0 rows) -- gtt_owner creates a GTT using its documented privileges. \c - gtt_owner CREATE /*GLOBAL*/ TEMPORARY TABLE t_security_grants (id integer, lbl text) ON COMMIT PRESERVE ROWS; INSERT INTO t_security_grants VALUES (1, 'one'); SELECT * FROM t_security_grants; id | lbl ----+----- 1 | one (1 row) -- gtt_other (no special grants, PUBLIC-equivalent) can still SELECT -- the catalog and the template table directly... \c - gtt_other SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; nspname | relname | preserved -------------+-------------------+----------- pgtt_schema | t_security_grants | t (1 row) SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_security_grants; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; -- ...but can no longer write to either. Every statement below must -- fail with a permission error. INSERT INTO pgtt_schema.pg_global_temp_tables VALUES (0, 'pgtt_schema', 'spoofed_gtt', true, 'id integer'); ERROR: permission denied for table pg_global_temp_tables UPDATE pgtt_schema.pg_global_temp_tables SET code = 'hijacked' WHERE relname = 't_security_grants'; ERROR: permission denied for table pg_global_temp_tables DELETE FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; ERROR: permission denied for table pg_global_temp_tables TRUNCATE pgtt_schema.pg_global_temp_tables; ERROR: permission denied for table pg_global_temp_tables SET pgtt.enabled TO off; INSERT INTO pgtt_schema.t_security_grants VALUES (99, 'should not be allowed'); ERROR: permission denied for table t_security_grants UPDATE pgtt_schema.t_security_grants SET lbl = 'hijacked'; ERROR: permission denied for table t_security_grants DELETE FROM pgtt_schema.t_security_grants; ERROR: permission denied for table t_security_grants TRUNCATE pgtt_schema.t_security_grants; ERROR: permission denied for table t_security_grants SET pgtt.enabled TO on; -- The registration and the data gtt_owner owns must be completely -- unaffected by the rejected attempts above. \c - gtt_owner SELECT * FROM t_security_grants; id | lbl ----+----- (0 rows) SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; nspname | relname | preserved -------------+-------------------+----------- pgtt_schema | t_security_grants | t (1 row) -- Cleanup. Reconnect first, a GTT can only be dropped from a session -- that has not already materialized it. \c - gtt_owner DROP TABLE t_security_grants; darold-pgtt-fe8b2d5/test/expected/16_sql_injection.out000066400000000000000000000051171522714605300231250ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a GTT name containing an embedded single quote cannot -- break out of pgtt's internally generated SPI queries and inject -- additional SQL. Previously several of those queries interpolated -- the table name straight into a single-quoted string literal -- without escaping; a double-quoted identifier containing a quote -- character could break out of the literal. -- ---- -- A canary table pgtt's internal queries have no legitimate reason to -- touch. If the old, unescaped code path were still active, the -- payload embedded in the GTT name below would empty this table out -- from under us during CREATE GLOBAL TEMPORARY TABLE. CREATE TABLE canary (id integer); INSERT INTO canary VALUES (1); -- The GTT name below is a valid, if unusual, double-quoted identifier -- containing a single quote and a complete extra SQL statement. This -- must be treated purely as a table name, not executed as SQL. CREATE GLOBAL TEMPORARY TABLE "x'; DELETE FROM canary WHERE '1'='1" (id integer, lbl text) ON COMMIT PRESERVE ROWS; WARNING: GLOBAL is deprecated in temporary table creation LINE 1: CREATE GLOBAL TEMPORARY TABLE "x'; DELETE FROM canary WHERE ... ^ -- The canary must be untouched. SELECT * FROM canary; id ---- 1 (1 row) -- The GTT must be registered under its exact, literal name. SELECT relname FROM pgtt_schema.pg_global_temp_tables WHERE relname = 'x''; DELETE FROM canary WHERE ''1''=''1'; relname ------------------------------------- x'; DELETE FROM canary WHERE '1'='1 (1 row) -- Renaming to another maliciously quoted name must be equally safe -- (exercises the UPDATE ... SET relname = ... path). This must be -- done before the GTT is materialized in this session, exactly as -- in 04_rename.sql, since renaming an already-materialized GTT is -- independently rejected for unrelated reasons. ALTER TABLE "x'; DELETE FROM canary WHERE '1'='1" RENAME TO "y'; TRUNCATE canary; --"; SELECT * FROM canary; id ---- 1 (1 row) SELECT relname FROM pgtt_schema.pg_global_temp_tables WHERE relname = 'y''; TRUNCATE canary; --'; relname ------------------------- y'; TRUNCATE canary; -- (1 row) -- The GTT must work completely normally despite its name. INSERT INTO "y'; TRUNCATE canary; --" VALUES (1, 'test'); SELECT * FROM "y'; TRUNCATE canary; --"; id | lbl ----+------ 1 | test (1 row) -- The canary must still be untouched after normal use too. SELECT * FROM canary; id ---- 1 (1 row) -- Cleanup \c - - DROP TABLE "y'; TRUNCATE canary; --"; DROP TABLE canary; darold-pgtt-fe8b2d5/test/expected/17_drop_authorization.out000066400000000000000000000054341522714605300242130ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a role which is not the owner of a Global Temporary -- Table cannot deregister it (remove its row from -- pg_global_temp_tables) simply by issuing a DROP TABLE statement -- that names it, even when that role's backend hasn't cached the -- GTT locally yet and so takes the "not cached" lookup path. -- -- Previously that path deleted the catalog row unconditionally, -- before PostgreSQL's own ownership check on the DROP TABLE -- statement itself ever ran. -- ---- -- Remember the user pg_regress connected with, the cleanup at the end of -- this file must be run as this user and not as one of the test roles. \set orig_user :USER GRANT ALL ON SCHEMA pgtt_schema TO gtt_owner; GRANT SELECT, INSERT, UPDATE, DELETE ON pgtt_schema.pg_global_temp_tables TO gtt_owner; GRANT USAGE ON SCHEMA pgtt_schema TO gtt_other; -- gtt_other connects first and runs an unrelated statement, so its -- backend's GTT cache gets initialized *before* the table below -- exists -- this is what forces the later DROP TABLE attempt onto -- the "not cached" lookup path rather than the ordinary cached path. \c - gtt_other SELECT 1; ?column? ---------- 1 (1 row) -- gtt_owner creates the GTT that gtt_other's cache predates. \c - gtt_owner CREATE /*GLOBAL*/ TEMPORARY TABLE t_drop_authorization (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- gtt_other (same connection as above, stale cache, not the owner) -- must NOT be able to drop it. \c - gtt_other DROP TABLE t_drop_authorization; ERROR: must be owner of table t_drop_authorization -- The registration must be completely unaffected by the rejected -- attempt above, and gtt_owner must still be able to use and drop it -- normally. \c - gtt_owner SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_drop_authorization'; nspname | relname | preserved -------------+----------------------+----------- pgtt_schema | t_drop_authorization | t (1 row) INSERT INTO t_drop_authorization VALUES (1, 'one'); SELECT * FROM t_drop_authorization; id | lbl ----+----- 1 | one (1 row) -- Reconnect (same owner) before dropping, matching the project's -- existing convention that a GTT can only be dropped from a session -- that hasn't already materialized it. \c - gtt_owner DROP TABLE t_drop_authorization; -- Cleanup the roles created by 15_security_grants. Roles are cluster wide -- objects and pg_regress only drops and recreates the regression database, -- so leaving them behind makes any further run of the test suite on the -- same instance fail with "role already exists". \c - :orig_user REVOKE ALL ON SCHEMA pgtt_schema FROM gtt_owner, gtt_other; REVOKE ALL ON pgtt_schema.pg_global_temp_tables FROM gtt_owner; DROP ROLE gtt_owner; DROP ROLE gtt_other; darold-pgtt-fe8b2d5/test/expected/18_subquery.out000066400000000000000000000046111522714605300221430ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a GTT referenced in a sub-query, a CTE or as a secondary -- FROM item is rerouted to the temporary table too, and not left -- pointing to the empty "template" table. -- -- See https://github.com/darold/pgtt/issues/58 -- ---- -- Create a regular table and a GTT with a primary key CREATE TABLE t_regular (id integer PRIMARY KEY, lbl text); CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer PRIMARY KEY, lbl text) ON COMMIT DELETE ROWS; -- The trigger replicates the rows of the regular table into the GTT -- using a NOT EXISTS sub-query on the GTT itself CREATE FUNCTION t_regular_trigger() RETURNS trigger AS $$ BEGIN INSERT INTO t_glob_temptable1 (id, lbl) SELECT t.id, t.lbl FROM t_regular t WHERE NOT EXISTS ( SELECT 1 FROM t_glob_temptable1 g WHERE g.id = t.id ); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER t_regular_trigger AFTER INSERT ON t_regular FOR EACH STATEMENT EXECUTE PROCEDURE t_regular_trigger(); BEGIN; -- Both inserts must succeed, the second call of the trigger must not -- try to insert again the row already stored in the temporary table INSERT INTO t_regular (id, lbl) VALUES (1, 'One'); INSERT INTO t_regular (id, lbl) VALUES (2, 'Two'); -- The GTT must contain the two rows SELECT * FROM t_glob_temptable1 ORDER BY id; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- The "template" table must still be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | lbl ----+----- (0 rows) SET pgtt.enabled TO on; COMMIT; -- ON COMMIT DELETE ROWS, the GTT must be empty now SELECT count(*) FROM t_glob_temptable1; count ------- 0 (1 row) BEGIN; INSERT INTO t_glob_temptable1 VALUES (1, 'One'), (2, 'Two'); -- GTT used as second FROM item SELECT g.id, g.lbl FROM t_regular r, t_glob_temptable1 g WHERE r.id = g.id ORDER BY 1; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- GTT used in a CTE WITH c AS (SELECT id, lbl FROM t_glob_temptable1) SELECT * FROM c ORDER BY id; id | lbl ----+----- 1 | One 2 | Two (2 rows) -- GTT used in a scalar sub-query only, the top level range table is empty SELECT (SELECT count(*) FROM t_glob_temptable1); count ------- 2 (1 row) ROLLBACK; -- Reconnect and cleanup \c - - DROP TABLE t_regular; DROP FUNCTION t_regular_trigger(); DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/expected/19_trigger.out000066400000000000000000000063241522714605300217330ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that the triggers defined on a GTT are replicated on the -- temporary table created on the fly for the session, together with -- their enabled state. -- -- See https://github.com/darold/pgtt/issues/52 -- ---- -- Trigger functions used by the triggers defined on the GTT CREATE FUNCTION t_copy() RETURNS trigger AS $$ BEGIN NEW.descr := NEW.a_code; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE FUNCTION t_stamp() RETURNS trigger AS $$ BEGIN NEW.descr := 'STAMPED'; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Create a GTT and define two triggers on it CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, a_code character(3), descr text) ON COMMIT PRESERVE ROWS; CREATE TRIGGER t_copy_trg BEFORE INSERT ON pgtt_schema.t_glob_temptable1 FOR EACH ROW EXECUTE PROCEDURE t_copy(); CREATE TRIGGER t_stamp_trg BEFORE INSERT ON pgtt_schema.t_glob_temptable1 FOR EACH ROW EXECUTE PROCEDURE t_stamp(); -- Use a non default state for both triggers ALTER TABLE pgtt_schema.t_glob_temptable1 ENABLE ALWAYS TRIGGER t_copy_trg; ALTER TABLE pgtt_schema.t_glob_temptable1 DISABLE TRIGGER t_stamp_trg; -- The triggers are attached to the "template" table SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' AND NOT t.tgisinternal ORDER BY 1; tgname | tgenabled -------------+----------- t_copy_trg | A t_stamp_trg | D (2 rows) -- The first insert creates the temporary table, the triggers must have been -- copied on it. Only the t_copy_trg trigger is enabled, descr must be set -- to the a_code value and not to the value set by the disabled trigger. INSERT INTO t_glob_temptable1 (id, a_code) VALUES (1, 'PQR'); SELECT * FROM t_glob_temptable1; id | a_code | descr ----+--------+------- 1 | PQR | PQR (1 row) -- Both triggers must be present on the temporary table with the same state SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg\_temp%' AND NOT t.tgisinternal ORDER BY 1; tgname | tgenabled -------------+----------- t_copy_trg | A t_stamp_trg | D (2 rows) -- The "template" table must still be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; id | a_code | descr ----+--------+------- (0 rows) SET pgtt.enabled TO on; -- Reconnect, the temporary table is dropped and a new one must be -- created with the triggers as well \c - - INSERT INTO t_glob_temptable1 (id, a_code) VALUES (2, 'XYZ'); SELECT * FROM t_glob_temptable1; id | a_code | descr ----+--------+------- 2 | XYZ | XYZ (1 row) SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg\_temp%' AND NOT t.tgisinternal ORDER BY 1; tgname | tgenabled -------------+----------- t_copy_trg | A t_stamp_trg | D (2 rows) -- Reconnect and cleanup \c - - DROP TABLE t_glob_temptable1; DROP FUNCTION t_copy(); DROP FUNCTION t_stamp(); darold-pgtt-fe8b2d5/test/expected/privilege.out000066400000000000000000000012331522714605300217370ustar00rootroot00000000000000CREATE ROLE CREATE EXTENSION GRANT DO You are now connected to database "gtt_privilege" as user "pgtt_user1". search_path -------------------- public,pgtt_schema (1 row) CREATE TABLE nspname | relname | preserved | code -------------+-------------------+-----------+---------------------- pgtt_schema | t_glob_temptable1 | t | id integer, lbl text (1 row) INSERT 0 1 INSERT 0 1 nspname | relname -------------+------------------- pgtt_schema | t_glob_temptable1 pg_temp_4 | t_glob_temptable1 (2 rows) SET id | lbl ----+----- (0 rows) SET id | lbl ----+----- 1 | One 2 | two (2 rows) darold-pgtt-fe8b2d5/test/privilege.sql000077500000000000000000000036261522714605300201410ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- This test is not part of the regression tests run with -- make check install because it need manual changes. -- -- Test for extension privilege to use non-superuser role. -- For this test the extension must have been registered as -- a plugin to allow simple user to use LOAD of the extension. -- See Installation instruction in documentation for details. -- Then execute: -- -- createdb gtt_privilege -- LANG=C psql -d gtt_privilege -f test/privilege.sql > results/privilege.out 2>&1 -- diff results/privilege.out test/expected/privilege.out -- dropdb gtt_privilege -- dropuser pgtt_user1 -- ---- -- As superuser CREATE ROLE pgtt_user1 LOGIN; CREATE EXTENSION IF NOT EXISTS pgtt; GRANT ALL ON SCHEMA pgtt_schema TO pgtt_user1; -- Set session_preload_libraries DO $$ BEGIN --EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''$libdir/plugins/pgtt''', current_database()); EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''pgtt''', current_database()); END $$; \c - pgtt_user1 -- Import the library -- LOAD '$libdir/plugins/pgtt'; SHOW search_path; -- Create a GTT like table with ON COMMIT PRESERVE ROWS CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'two'); -- Look if we have two tables now SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; SELECT * FROM t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/000077500000000000000000000000001522714605300162175ustar00rootroot00000000000000darold-pgtt-fe8b2d5/test/sql/00_init.sql000066400000000000000000000011751522714605300202060ustar00rootroot00000000000000/* * Must be executed before all regression test. */ -- Create the PostgreSQL extension CREATE EXTENSION pgtt; -- Set session_preload_libraries DO $$ BEGIN EXECUTE format('ALTER DATABASE %I SET session_preload_libraries = ''pgtt''', current_database()); END $$; -- Create a regular table with some rows CREATE TABLE source (id serial PRIMARY KEY, c2 varchar(50) UNIQUE NOT NULL, lbl varchar DEFAULT '-'); COMMENT ON TABLE source IS 'Table used to demonstrate GTT create as feature'; COMMENT ON COLUMN source.id IS 'auto generated column'; CREATE INDEX ON source(lbl); INSERT INTO source VALUES (1,'one'), (2,'two'),(3,'three'); darold-pgtt-fe8b2d5/test/sql/01_oncommitdelete.sql000066400000000000000000000044621522714605300222560ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with ON COMMIT DELETE ROWS clause. -- -- Test drop of GTT when in use throwing error and effective -- when done in a separate session. -- ---- -- Create a GTT like table to test ON COMMIT DELETE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT DELETE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Second insert, the temporary table exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; COMMIT; -- No row must perstist after the commit SELECT * FROM t_glob_temptable1; -- With holdabe cursor the rows must remain BEGIN; INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); DECLARE curs1 CURSOR WITH HOLD FOR SELECT * FROM t_glob_temptable1; FETCH curs1; COMMIT; -- No row must perstist after the commit for the cursor FETCH curs1; -- But not for a new select SELECT * FROM t_glob_temptable1; CLOSE curs1; -- Drop the global temporary table: fail because it is in use DROP TABLE t_glob_temptable1; -- Reconnect and drop it \c - - SHOW search_path; DROP TABLE t_glob_temptable1; VACUUM pg_class; SELECT pg_sleep(1); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- should be empty -- The "template" unlogged table should not exists anymore SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; darold-pgtt-fe8b2d5/test/sql/02_oncommitpreserve.sql000066400000000000000000000027351522714605300226510ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with ON COMMIT PRESERVE ROWS clause. -- -- Test rereouting of truncate on the temporary table. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Second insert, the temporary table exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; COMMIT; SELECT * FROM t_glob_temptable1; -- Truncate GTT TRUNCATE t_glob_temptable1; SELECT * FROM t_glob_temptable1; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/03_createontruncate.sql000066400000000000000000000040531522714605300226120ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for TRUNCATE on GTT with temporary table first access in a transaction. -- The temporary table must not persist after the transaction rollback. -- ---- -- Create a GTT like table CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; BEGIN; -- Truncate first will not create the temporary table, it will -- be operated on the "template" table which will do nothing. TRUNCATE t_glob_temptable1; -- Look if we have two tables now SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; -- Now truncate the temporary table TRUNCATE t_glob_temptable1; -- Verify that there is no mo rows SELECT * FROM t_glob_temptable1; ROLLBACK; -- The "template" unlogged table exists, but the -- temporary table not because of the rollback SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; -- Reconnect to test locking, see #41 \c - - -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE test_gtt (id int, lbl text); SELECT * FROM pgtt_schema.test_gtt; SELECT * FROM pgtt_schema.test_gtt; -- success \c - - -- Cleanup DROP TABLE test_gtt; darold-pgtt-fe8b2d5/test/sql/04_rename.sql000066400000000000000000000106461522714605300205210ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on renaming GTT using ALTER TABLE ... RENAME TO ... -- -- Renaming a GGT when the temporary table has already been -- created is not allowed and must be done in a new session. -- ---- -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; CREATE INDEX ON pgtt_schema.t_glob_temptable1 (id); CREATE INDEX ON pgtt_schema.t_glob_temptable1 (lbl); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- With indexes defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; -- Rename the table ALTER TABLE t_glob_temptable1 RENAME TO t_glob_temptable2; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable2' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- With indexes still defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable2' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; -- With the first insert a temporary table is created and the row inseterd SET pgtt.enabled TO on; INSERT INTO t_glob_temptable2 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable2'; -- The table doesn't exist anymore SET pgtt.enabled TO off; \d t_glob_temptable1; SET pgtt.enabled TO on; INSERT INTO t_glob_temptable2 VALUES (2, 'two'); SELECT * FROM t_glob_temptable2 WHERE id = 2; -- Rename the table when the temporary table has already been created is not allowed ALTER TABLE t_glob_temptable2 RENAME TO t_glob_temptable1; \c - - ALTER TABLE t_glob_temptable2 RENAME TO t_glob_temptable1; -- Look if the renaming is effective SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/05_useindex.sql000066400000000000000000000077651522714605300211070ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with index, the temporary table must have the -- index too. -- ---- -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; SET pgtt.enabled TO off; CREATE INDEX ON pgtt_schema.t_glob_temptable1 (id); CREATE INDEX ON pgtt_schema.t_glob_temptable1 (lbl); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- With indexes still defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; SET pgtt.enabled TO on; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- and that the temporary table has the indexes too SELECT tablename, indexname FROM pg_indexes WHERE tablename = 't_glob_temptable1' and schemaname LIKE 'pg_temp_%' ORDER BY tablename, indexname; INSERT INTO t_glob_temptable1 VALUES (2, 'two'); -- Verify that the index is used SET enable_bitmapscan TO off; EXPLAIN (COSTS OFF) SELECT * FROM t_glob_temptable1 WHERE id = 2; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable2 (id integer, lbl text) ON COMMIT PRESERVE ROWS; CREATE INDEX ON t_glob_temptable2 (id); SELECT * FROM t_glob_temptable2; -- Must complain that the GTT is in use CREATE INDEX ON t_glob_temptable2 (lbl); SELECT pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = 'pgtt_schema.t_glob_temptable2'::regclass::oid AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; -- Check that we do not break LIKE ... USING INDEXES CREATE TABLE tb_with_index (id integer PRIMARY KEY, lbl varchar); CREATE INDEX ON tb_with_index(lbl); CREATE TEMPORARY TABLE temptb_with_index (LIKE tb_with_index INCLUDING INDEXES); SELECT c2.relname, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true) FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = 'temptb_with_index'::regclass AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; \c - - DROP TABLE t_glob_temptable2; darold-pgtt-fe8b2d5/test/sql/06_createas.sql000066400000000000000000000025771522714605300210470ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with TABLE ... AS clause. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 AS SELECT * FROM source WITH DATA; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists as well as -- the temporary table as we have used WITH DATA SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at the temporary table itself, it must have the rows SELECT * FROM t_glob_temptable1 ORDER BY id; BEGIN; -- With a new row INSERT INTO t_glob_temptable1 VALUES (4, 'four'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1 ORDER BY id; COMMIT; SELECT * FROM t_glob_temptable1 ORDER BY id; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/07_createlike.sql000066400000000000000000000075371522714605300213720ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT with TABLE ... (LIKE ...) clause. -- ---- -- Create a GTT like table to test ON COMMIT PRESERVE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 ( LIKE source INCLUDING ALL ) ON COMMIT PRESERVE ROWS; -- Look at table description SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- With indexes defined SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look at temp table description SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg_temp_%' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), pg_catalog.pg_get_constraintdef(con.oid, true), contype FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) WHERE c.oid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg_temp_%' ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, c2.relname; -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1 ORDER BY id; COMMIT; SELECT * FROM t_glob_temptable1 ORDER BY id; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/08_plplgsql.sql000066400000000000000000000061011522714605300211030ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for GTT defined inside a PLPGSQL function. -- ---- CREATE OR REPLACE FUNCTION test_temp_table () RETURNS integer AS $$ DECLARE nrows integer; BEGIN CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1(id int, lbl text) ON COMMIT PRESERVE ROWS; INSERT INTO t_glob_temptable1 (id, lbl) SELECT i, md5(i::text) FROM generate_series(1, 10) i; SELECT count(*) INTO nrows FROM t_glob_temptable1 ; RETURN nrows; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Look at Global Temporary Table definition: none SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- Call the function, must returns 10 rows SELECT test_temp_table(); -- Look at Global Temporary Table definition: table exists SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- Look if the temporary table exists outside the function call SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- A "template" unlogged table should exists SET pgtt.enabled TO off; SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- Get rows from the template table SELECT * FROM pgtt_schema.t_glob_temptable1; -- Get rows from the temporary table SET pgtt.enabled TO on; SELECT * FROM t_glob_temptable1; -- Reconnect without dropping the global temporary table \c - - SET pgtt.enabled TO off; VACUUM pg_class; SELECT pg_sleep(1); -- Verify that only the temporary table have been dropped -- Only the "template" unlogged table should exists SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Look at Global Temporary Table definition, the table must be present SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; SET pgtt.enabled TO on; -- Call the function a second time - must fail the table already exists SELECT test_temp_table(); -- Look at temporary table content, must be empty after the reconnect and function failure SELECT * FROM t_glob_temptable1; -- Now the "template" unlogged table should exists as well as the temporary table SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; DROP FUNCTION test_temp_table(); darold-pgtt-fe8b2d5/test/sql/09_transaction.sql000066400000000000000000000036621522714605300216040ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for transaction manamgement on GTT. -- -- Test that the creation a GTT in rollbacked transaction -- will not preserve it. -- ---- BEGIN; -- Register the Global temporary table in a transaction CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look at content of the template for Global Temporary Table, must be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Look at content of the Global Temporary Table SELECT * FROM t_glob_temptable1; ROLLBACK; -- The GTT must not exists SELECT * FROM t_glob_temptable1; -- Return nothing SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Register the Global temporary table outside the transaction CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; BEGIN; -- Drop the GTT DROP TABLE t_glob_temptable1; ROLLBACK; -- Insert some value will create the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- The GTT must not exists SELECT * FROM t_glob_temptable1; -- Both tables muste exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Reconnect and drop it \c - - -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/10_foreignkey.sql000066400000000000000000000027251522714605300214100ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test for unsupported foreign keys on GTT. -- ---- -- Import the library -- LOAD 'pgtt'; -- Must throw ERROR: attempt to create referential integrity constraint on temporary table. CREATE /*GLOBAL*/ TEMPORARY TABLE t2 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); BEGIN; CREATE /*GLOBAL*/ TEMPORARY TABLE t2 (c1 integer); -- Must throw ERROR: attempt to create referential integrity constraint on temporary table. ALTER TABLE t2 ADD FOREIGN KEY (c1) REFERENCES source (id); ROLLBACK; BEGIN; -- Must be valid statement CREATE TABLE t3 (c1 integer, FOREIGN KEY (c1) REFERENCES source (id)); ROLLBACK; ---- -- Regression test for the column-level REFERENCES form of a foreign -- key, which a previous text-regex implementation of this check -- (searching only for the literal substring "FOREIGN KEY") failed to -- catch, since this syntax contains no such substring. ---- -- Must throw the same error as the table-level FOREIGN KEY (...) form above. CREATE /*GLOBAL*/ TEMPORARY TABLE t4 (c1 integer REFERENCES source (id)); BEGIN; CREATE /*GLOBAL*/ TEMPORARY TABLE t4 (c1 integer); -- Column-level REFERENCES added later via ALTER TABLE must also fail, -- exactly like the table-level form tested above. ALTER TABLE t4 ADD FOREIGN KEY (c1) REFERENCES source (id); ROLLBACK; BEGIN; -- Must be valid statement: column-level REFERENCES on a regular table. CREATE TABLE t5 (c1 integer REFERENCES source (id)); ROLLBACK; darold-pgtt-fe8b2d5/test/sql/11_after_error.sql000066400000000000000000000041341522714605300215550ustar00rootroot00000000000000-- Import the library -- LOAD 'pgtt'; -- Create a GTT like table to test ON COMMIT DELETE ROWS CREATE GLOBAL TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT DELETE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; BEGIN; -- With the first insert some value in the temporary table INSERT INTO t_glob_temptable1 VALUES (1, 'One'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Verify the registering of the temporary table SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- Second insert failure INSERT INTO t_glob_temptable1 VALUES (2, two); ROLLBACK; -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Insert a new row BEGIN; -- With the first insert some value in the temporary table -- Should not return an error that table doesn't exists INSERT INTO t_glob_temptable1 VALUES (2, 'Two'); -- Look if we have two tables now SELECT regexp_replace(n.nspname, '\d+', 'x', 'g'), c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; -- Verify the insert SELECT * FROM t_glob_temptable1; COMMIT; -- Reconnect and drop the GTT \c - - -- LOAD 'pgtt'; SHOW search_path; DROP TABLE t_glob_temptable1; VACUUM pg_class; SELECT pg_sleep(1); -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- should be empty -- The "template" unlogged table should not exists anymore SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace=n.oid) WHERE relname = 't_glob_temptable1'; darold-pgtt-fe8b2d5/test/sql/12_droptable.sql000066400000000000000000000023051522714605300212160ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on dropping regular table when the extension is loaded -- ---- -- Import the library -- LOAD 'pgtt'; -- Create a GTT like table CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- Look at Global Temporary Table definition SELECT nspname, relname, preserved, code FROM pgtt_schema.pg_global_temp_tables; -- A "template" unlogged table should exists SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' ) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum; -- Create and drop a regular table CREATE TABLE t1 (id integer); DROP TABLE t1; -- Cleanup DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/13_searchpath.sql000066400000000000000000000016431522714605300213710ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test on search_path forced by the extension -- ---- DROP EXTENSION pgtt; SHOW search_path ; create schema first_schema; create schema second_schema; SET search_path TO "$user",public,pg_catalog,fisrt_schema,second_schema; SHOW search_path ; CREATE EXTENSION pgtt; SHOW search_path ; SET search_path TO pg_catalog; SHOW search_path ; SET search_path TO public; SHOW search_path ; -- Test only pg_catalog in search_path. DROP EXTENSION pgtt; \c - - SHOW search_path; SET search_path TO pg_catalog; SHOW search_path; CREATE EXTENSION pgtt; SHOW search_path; -- Test the pg_catalog at end of search_path. DROP EXTENSION pgtt; \c - - SET search_path TO "$user", public, pg_catalog; SHOW search_path; CREATE EXTENSION pgtt; SHOW search_path; -- Test empty search path like set by pg_dump SELECT pg_catalog.set_config('search_path', '', false); SHOW search_path; darold-pgtt-fe8b2d5/test/sql/14_concurrent_index.sql000066400000000000000000000035101522714605300226140ustar00rootroot00000000000000-- -- Test for CREATE INDEX CONCURRENTLY on pgtt -- -- Start with a clean slate to ensure consistent output SET client_min_messages TO WARNING; DROP EXTENSION IF EXISTS pgtt CASCADE; CREATE EXTENSION pgtt; RESET client_min_messages; -- 1. Create the Global Temporary Table CREATE GLOBAL TEMPORARY TABLE my_gtt_concurrent ( id int, lbl text ) ON COMMIT PRESERVE ROWS; -- 2. Activate the GTT -- We insert a row so that the local temporary table is physically created. -- This forces the namespace check in pgtt.c to see a "pg_temp" namespace. INSERT INTO my_gtt_concurrent VALUES (1, 'initial_data'); -- 3. Negative Test: Standard CREATE INDEX -- This should still FAIL because the GTT is active and we are not using CONCURRENTLY. -- The extension preserves the error here: if (!stmt->concurrent) ereport(ERROR... \set ON_ERROR_STOP 0 CREATE INDEX idx_gtt_fail ON my_gtt_concurrent (id); \set ON_ERROR_STOP 1 -- 4. Positive Test: CREATE INDEX CONCURRENTLY -- This should SUCCEED. The extension bypasses the error when stmt->concurrent is true -- and uses ShareUpdateExclusiveLock. CREATE INDEX CONCURRENTLY idx_gtt_success ON my_gtt_concurrent (id); -- Verify the index is actually there and valid SELECT CASE WHEN schemaname LIKE 'pg_temp_%' THEN 'pg_temp' ELSE schemaname END AS schemaname, tablename, indexname, regexp_replace( replace(indexdef, 'public.', 'pg_temp.'), 'pg_temp_\d+', 'pg_temp', 'g' ) as indexdef FROM pg_indexes WHERE tablename = 'my_gtt_concurrent' ORDER BY indexname; -- 5. Test REINDEX CONCURRENTLY -- Assuming the implementation handles ReindexStmt similarly to IndexStmt, -- or it relies on standard PG behavior for the reindex once the index exists. REINDEX INDEX CONCURRENTLY idx_gtt_success; -- No cleanup needed: pg_regress will destroy the environment. darold-pgtt-fe8b2d5/test/sql/15_security_grants.sql000066400000000000000000000057271522714605300225050ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that PUBLIC no longer has write access to the shared -- pg_global_temp_tables catalog table or to a GTT's "template" -- table, only SELECT -- and that a role provisioned per the -- documented non-superuser setup (CREATE on the pgtt schema, plus -- the additional explicit grant this fix now requires on -- pg_global_temp_tables) can still fully create, use and drop a GTT. -- ---- -- A role with no special pgtt privileges beyond the schema's default -- USAGE grant to PUBLIC (i.e. representative of PUBLIC itself). CREATE ROLE gtt_other LOGIN; -- A role provisioned exactly as the README's non-superuser setup -- instructions describe, plus the additional table-level grant this -- fix now requires (documented in the 4.5.0->4.5.1 upgrade script). CREATE ROLE gtt_owner LOGIN; GRANT ALL ON SCHEMA pgtt_schema TO gtt_owner; GRANT SELECT, INSERT, UPDATE, DELETE ON pgtt_schema.pg_global_temp_tables TO gtt_owner; -- Baseline (superuser) sanity check: the table only grants SELECT to -- PUBLIC now, not ALL. SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_schema = 'pgtt_schema' AND table_name = 'pg_global_temp_tables' AND grantee = 'public' ORDER BY privilege_type; -- gtt_owner creates a GTT using its documented privileges. \c - gtt_owner CREATE /*GLOBAL*/ TEMPORARY TABLE t_security_grants (id integer, lbl text) ON COMMIT PRESERVE ROWS; INSERT INTO t_security_grants VALUES (1, 'one'); SELECT * FROM t_security_grants; -- gtt_other (no special grants, PUBLIC-equivalent) can still SELECT -- the catalog and the template table directly... \c - gtt_other SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_security_grants; SET pgtt.enabled TO on; -- ...but can no longer write to either. Every statement below must -- fail with a permission error. INSERT INTO pgtt_schema.pg_global_temp_tables VALUES (0, 'pgtt_schema', 'spoofed_gtt', true, 'id integer'); UPDATE pgtt_schema.pg_global_temp_tables SET code = 'hijacked' WHERE relname = 't_security_grants'; DELETE FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; TRUNCATE pgtt_schema.pg_global_temp_tables; SET pgtt.enabled TO off; INSERT INTO pgtt_schema.t_security_grants VALUES (99, 'should not be allowed'); UPDATE pgtt_schema.t_security_grants SET lbl = 'hijacked'; DELETE FROM pgtt_schema.t_security_grants; TRUNCATE pgtt_schema.t_security_grants; SET pgtt.enabled TO on; -- The registration and the data gtt_owner owns must be completely -- unaffected by the rejected attempts above. \c - gtt_owner SELECT * FROM t_security_grants; SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_security_grants'; -- Cleanup. Reconnect first, a GTT can only be dropped from a session -- that has not already materialized it. \c - gtt_owner DROP TABLE t_security_grants; darold-pgtt-fe8b2d5/test/sql/16_sql_injection.sql000066400000000000000000000042021522714605300221050ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a GTT name containing an embedded single quote cannot -- break out of pgtt's internally generated SPI queries and inject -- additional SQL. Previously several of those queries interpolated -- the table name straight into a single-quoted string literal -- without escaping; a double-quoted identifier containing a quote -- character could break out of the literal. -- ---- -- A canary table pgtt's internal queries have no legitimate reason to -- touch. If the old, unescaped code path were still active, the -- payload embedded in the GTT name below would empty this table out -- from under us during CREATE GLOBAL TEMPORARY TABLE. CREATE TABLE canary (id integer); INSERT INTO canary VALUES (1); -- The GTT name below is a valid, if unusual, double-quoted identifier -- containing a single quote and a complete extra SQL statement. This -- must be treated purely as a table name, not executed as SQL. CREATE GLOBAL TEMPORARY TABLE "x'; DELETE FROM canary WHERE '1'='1" (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- The canary must be untouched. SELECT * FROM canary; -- The GTT must be registered under its exact, literal name. SELECT relname FROM pgtt_schema.pg_global_temp_tables WHERE relname = 'x''; DELETE FROM canary WHERE ''1''=''1'; -- Renaming to another maliciously quoted name must be equally safe -- (exercises the UPDATE ... SET relname = ... path). This must be -- done before the GTT is materialized in this session, exactly as -- in 04_rename.sql, since renaming an already-materialized GTT is -- independently rejected for unrelated reasons. ALTER TABLE "x'; DELETE FROM canary WHERE '1'='1" RENAME TO "y'; TRUNCATE canary; --"; SELECT * FROM canary; SELECT relname FROM pgtt_schema.pg_global_temp_tables WHERE relname = 'y''; TRUNCATE canary; --'; -- The GTT must work completely normally despite its name. INSERT INTO "y'; TRUNCATE canary; --" VALUES (1, 'test'); SELECT * FROM "y'; TRUNCATE canary; --"; -- The canary must still be untouched after normal use too. SELECT * FROM canary; -- Cleanup \c - - DROP TABLE "y'; TRUNCATE canary; --"; DROP TABLE canary; darold-pgtt-fe8b2d5/test/sql/17_drop_authorization.sql000066400000000000000000000050131522714605300231720ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a role which is not the owner of a Global Temporary -- Table cannot deregister it (remove its row from -- pg_global_temp_tables) simply by issuing a DROP TABLE statement -- that names it, even when that role's backend hasn't cached the -- GTT locally yet and so takes the "not cached" lookup path. -- -- Previously that path deleted the catalog row unconditionally, -- before PostgreSQL's own ownership check on the DROP TABLE -- statement itself ever ran. -- ---- -- Remember the user pg_regress connected with, the cleanup at the end of -- this file must be run as this user and not as one of the test roles. \set orig_user :USER GRANT ALL ON SCHEMA pgtt_schema TO gtt_owner; GRANT SELECT, INSERT, UPDATE, DELETE ON pgtt_schema.pg_global_temp_tables TO gtt_owner; GRANT USAGE ON SCHEMA pgtt_schema TO gtt_other; -- gtt_other connects first and runs an unrelated statement, so its -- backend's GTT cache gets initialized *before* the table below -- exists -- this is what forces the later DROP TABLE attempt onto -- the "not cached" lookup path rather than the ordinary cached path. \c - gtt_other SELECT 1; -- gtt_owner creates the GTT that gtt_other's cache predates. \c - gtt_owner CREATE /*GLOBAL*/ TEMPORARY TABLE t_drop_authorization (id integer, lbl text) ON COMMIT PRESERVE ROWS; -- gtt_other (same connection as above, stale cache, not the owner) -- must NOT be able to drop it. \c - gtt_other DROP TABLE t_drop_authorization; -- The registration must be completely unaffected by the rejected -- attempt above, and gtt_owner must still be able to use and drop it -- normally. \c - gtt_owner SELECT nspname, relname, preserved FROM pgtt_schema.pg_global_temp_tables WHERE relname = 't_drop_authorization'; INSERT INTO t_drop_authorization VALUES (1, 'one'); SELECT * FROM t_drop_authorization; -- Reconnect (same owner) before dropping, matching the project's -- existing convention that a GTT can only be dropped from a session -- that hasn't already materialized it. \c - gtt_owner DROP TABLE t_drop_authorization; -- Cleanup the roles created by 15_security_grants. Roles are cluster wide -- objects and pg_regress only drops and recreates the regression database, -- so leaving them behind makes any further run of the test suite on the -- same instance fail with "role already exists". \c - :orig_user REVOKE ALL ON SCHEMA pgtt_schema FROM gtt_owner, gtt_other; REVOKE ALL ON pgtt_schema.pg_global_temp_tables FROM gtt_owner; DROP ROLE gtt_owner; DROP ROLE gtt_other; darold-pgtt-fe8b2d5/test/sql/18_subquery.sql000066400000000000000000000042361522714605300211340ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that a GTT referenced in a sub-query, a CTE or as a secondary -- FROM item is rerouted to the temporary table too, and not left -- pointing to the empty "template" table. -- -- See https://github.com/darold/pgtt/issues/58 -- ---- -- Create a regular table and a GTT with a primary key CREATE TABLE t_regular (id integer PRIMARY KEY, lbl text); CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer PRIMARY KEY, lbl text) ON COMMIT DELETE ROWS; -- The trigger replicates the rows of the regular table into the GTT -- using a NOT EXISTS sub-query on the GTT itself CREATE FUNCTION t_regular_trigger() RETURNS trigger AS $$ BEGIN INSERT INTO t_glob_temptable1 (id, lbl) SELECT t.id, t.lbl FROM t_regular t WHERE NOT EXISTS ( SELECT 1 FROM t_glob_temptable1 g WHERE g.id = t.id ); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER t_regular_trigger AFTER INSERT ON t_regular FOR EACH STATEMENT EXECUTE PROCEDURE t_regular_trigger(); BEGIN; -- Both inserts must succeed, the second call of the trigger must not -- try to insert again the row already stored in the temporary table INSERT INTO t_regular (id, lbl) VALUES (1, 'One'); INSERT INTO t_regular (id, lbl) VALUES (2, 'Two'); -- The GTT must contain the two rows SELECT * FROM t_glob_temptable1 ORDER BY id; -- The "template" table must still be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; COMMIT; -- ON COMMIT DELETE ROWS, the GTT must be empty now SELECT count(*) FROM t_glob_temptable1; BEGIN; INSERT INTO t_glob_temptable1 VALUES (1, 'One'), (2, 'Two'); -- GTT used as second FROM item SELECT g.id, g.lbl FROM t_regular r, t_glob_temptable1 g WHERE r.id = g.id ORDER BY 1; -- GTT used in a CTE WITH c AS (SELECT id, lbl FROM t_glob_temptable1) SELECT * FROM c ORDER BY id; -- GTT used in a scalar sub-query only, the top level range table is empty SELECT (SELECT count(*) FROM t_glob_temptable1); ROLLBACK; -- Reconnect and cleanup \c - - DROP TABLE t_regular; DROP FUNCTION t_regular_trigger(); DROP TABLE t_glob_temptable1; darold-pgtt-fe8b2d5/test/sql/19_trigger.sql000066400000000000000000000053751522714605300207260ustar00rootroot00000000000000---- -- Regression test to Global Temporary Table implementation -- -- Test that the triggers defined on a GTT are replicated on the -- temporary table created on the fly for the session, together with -- their enabled state. -- -- See https://github.com/darold/pgtt/issues/52 -- ---- -- Trigger functions used by the triggers defined on the GTT CREATE FUNCTION t_copy() RETURNS trigger AS $$ BEGIN NEW.descr := NEW.a_code; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE FUNCTION t_stamp() RETURNS trigger AS $$ BEGIN NEW.descr := 'STAMPED'; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Create a GTT and define two triggers on it CREATE /*GLOBAL*/ TEMPORARY TABLE t_glob_temptable1 (id integer, a_code character(3), descr text) ON COMMIT PRESERVE ROWS; CREATE TRIGGER t_copy_trg BEFORE INSERT ON pgtt_schema.t_glob_temptable1 FOR EACH ROW EXECUTE PROCEDURE t_copy(); CREATE TRIGGER t_stamp_trg BEFORE INSERT ON pgtt_schema.t_glob_temptable1 FOR EACH ROW EXECUTE PROCEDURE t_stamp(); -- Use a non default state for both triggers ALTER TABLE pgtt_schema.t_glob_temptable1 ENABLE ALWAYS TRIGGER t_copy_trg; ALTER TABLE pgtt_schema.t_glob_temptable1 DISABLE TRIGGER t_stamp_trg; -- The triggers are attached to the "template" table SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname = 'pgtt_schema' AND NOT t.tgisinternal ORDER BY 1; -- The first insert creates the temporary table, the triggers must have been -- copied on it. Only the t_copy_trg trigger is enabled, descr must be set -- to the a_code value and not to the value set by the disabled trigger. INSERT INTO t_glob_temptable1 (id, a_code) VALUES (1, 'PQR'); SELECT * FROM t_glob_temptable1; -- Both triggers must be present on the temporary table with the same state SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg\_temp%' AND NOT t.tgisinternal ORDER BY 1; -- The "template" table must still be empty SET pgtt.enabled TO off; SELECT * FROM pgtt_schema.t_glob_temptable1; SET pgtt.enabled TO on; -- Reconnect, the temporary table is dropped and a new one must be -- created with the triggers as well \c - - INSERT INTO t_glob_temptable1 (id, a_code) VALUES (2, 'XYZ'); SELECT * FROM t_glob_temptable1; SELECT t.tgname, t.tgenabled FROM pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (n.oid = c.relnamespace) WHERE c.relname = 't_glob_temptable1' AND n.nspname LIKE 'pg\_temp%' AND NOT t.tgisinternal ORDER BY 1; -- Reconnect and cleanup \c - - DROP TABLE t_glob_temptable1; DROP FUNCTION t_copy(); DROP FUNCTION t_stamp(); darold-pgtt-fe8b2d5/updates/000077500000000000000000000000001522714605300161065ustar00rootroot00000000000000darold-pgtt-fe8b2d5/updates/pgtt--2.0.0--2.1.0.sql000066400000000000000000000004761522714605300210350ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.1.0--2.2.0.sql000066400000000000000000000004761522714605300210370ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.10.0--3.0.0.sql000066400000000000000000000004761522714605300211160ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.2.0--2.3.0.sql000066400000000000000000000004761522714605300210410ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.3.0--2.4.0.sql000066400000000000000000000004761522714605300210430ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.4.0--2.5.0.sql000066400000000000000000000004761522714605300210450ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.5.0--2.6.0.sql000066400000000000000000000004761522714605300210470ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.6.0--2.7.0.sql000066400000000000000000000004761522714605300210510ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.7.0--2.8.0.sql000066400000000000000000000004761522714605300210530ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.8.0--2.9.0.sql000066400000000000000000000004761522714605300210550ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--2.9.0--2.10.0.sql000066400000000000000000000004761522714605300211260ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--3.0.0--3.1.0.sql000066400000000000000000000004761522714605300210370ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--3.1.0--3.2.0.sql000066400000000000000000000004761522714605300210410ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--3.2.0--4.0.0.sql000066400000000000000000000004761522714605300210410ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--4.0.0--4.1.0.sql000066400000000000000000000004761522714605300210410ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--4.1.0--4.2.0.sql000066400000000000000000000004761522714605300210430ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--4.2.0--4.3.0.sql000066400000000000000000000004761522714605300210450ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--4.3.0--4.4.0.sql000066400000000000000000000004761522714605300210470ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit -- check the functions bodies as creation time, enabled by default SET LOCAL check_function_bodies = on ; -- make sure of client encoding SET LOCAL client_encoding = 'UTF8'; darold-pgtt-fe8b2d5/updates/pgtt--4.4.0--4.5.0.sql000066400000000000000000000002161522714605300210410ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit darold-pgtt-fe8b2d5/updates/pgtt--4.5.0--4.6.0.sql000066400000000000000000000027641522714605300210550ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pgtt UPDATE" to load this file. \quit ---- -- SECURITY FIX: tighten default privileges on pg_global_temp_tables. -- -- Every released version (2.0.0 through 4.5.0) granted ALL privileges -- on this table to PUBLIC. Because this table is the single shared -- bookkeeping catalog for every Global Temporary Table in the database -- (owner, structure "code", ON COMMIT behavior, ...), this let any -- authenticated role directly INSERT/UPDATE/DELETE/TRUNCATE rows that -- belong to GTTs created by other users, with no ownership check -- whatsoever -- entirely bypassing the ownership checks the extension -- performs when it intercepts CREATE/DROP/RENAME TABLE. -- -- SELECT is intentionally left available to PUBLIC: pgtt loads this -- entire table into every backend's local cache on first use -- regardless of who created which row (this is how GTTs remain usable -- database-wide, matching Oracle-style GTT semantics), and the table -- is documented as an introspectable catalog. -- -- Roles that create/rename/drop GTTs (i.e. any role the DBA has -- granted CREATE on the pgtt schema to, per the README) must now also -- be granted explicit write access on this table, for example: -- GRANT SELECT, INSERT, UPDATE, DELETE -- ON @extschema@.pg_global_temp_tables TO ; ---- REVOKE ALL ON TABLE @extschema@.pg_global_temp_tables FROM PUBLIC; GRANT SELECT ON TABLE @extschema@.pg_global_temp_tables TO PUBLIC;