├── .envrc ├── .github └── workflows │ ├── run_tests.yml │ └── update_cache.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── META.json ├── Makefile ├── README.md ├── doc ├── Makefile ├── _static │ ├── css │ │ └── custom.css │ └── img │ │ ├── glyphicons-142-database-plus.png │ │ ├── glyphicons-151-edit.png │ │ ├── glyphicons-352-book-open.png │ │ └── glyphicons-419-disk-import.png ├── api.rst ├── conf.py ├── contribute.rst ├── foreign-data-wrappers.rst ├── foreign-data-wrappers │ ├── csvfdw.rst │ ├── fsfdw.rst │ ├── imapfdw.rst │ ├── ldapfdw.rst │ ├── processfdw.rst │ ├── rssfdw.rst │ └── sqlalchemyfdw.rst ├── getting-started.rst ├── implementing-an-fdw.rst ├── implementing-tutorial.rst ├── index.rst ├── installation.rst ├── internals.rst ├── multicorn.md ├── multicorn_directives │ └── __init__.py ├── requirements.txt └── third-party-fdw.rst ├── docheck ├── docker └── py39-pg13 │ └── Dockerfile ├── flake.lock ├── flake.nix ├── multicorn.control ├── pyproject.toml ├── python └── multicorn │ ├── __init__.py │ ├── compat.py │ ├── csvfdw.py │ ├── fsfdw │ ├── __init__.py │ ├── docutils_meta.py │ ├── restfsfdw.py │ ├── structuredfs.py │ └── test.py │ ├── gcfdw.py │ ├── gitfdw.py │ ├── googlefdw.py │ ├── imapfdw.py │ ├── ldapfdw.py │ ├── processfdw.py │ ├── rssfdw.py │ ├── sqlalchemyfdw.py │ ├── statefdw.py │ ├── testfdw.py │ ├── utils.py │ └── xmlfdw.py ├── setup.py ├── sql ├── multicorn--1.4.0--2.2.sql ├── multicorn--1.4.0--2.3.sql ├── multicorn--2.2--2.3.sql ├── multicorn--2.2--2.4.sql ├── multicorn--2.2.sql ├── multicorn--2.3--2.4.sql ├── multicorn--2.3.sql ├── multicorn--2.4.sql ├── multicorn--2.5.sql ├── multicorn--3.0.sql └── multicorn.sql ├── src ├── errors.c ├── multicorn.c ├── multicorn.h ├── python.c ├── query.c └── utils.c ├── test-3.10 ├── test-3.11 ├── test-3.12 ├── test-3.13 ├── test-3.9 ├── expected │ ├── import_sqlalchemy.out │ ├── import_test.out │ ├── multicorn_alchemy_test.out │ ├── multicorn_cache_invalidation.out │ ├── multicorn_column_options_test.out │ ├── multicorn_error_test.out │ ├── multicorn_logger_test.out │ ├── multicorn_planner_test.out │ ├── multicorn_planner_test_1.out │ ├── multicorn_regression_test.out │ ├── multicorn_sequence_test.out │ ├── multicorn_test_date.out │ ├── multicorn_test_dict.out │ ├── multicorn_test_list.out │ ├── multicorn_test_sort.out │ ├── write_batch_test.out │ ├── write_filesystem.out │ ├── write_savepoints.out │ ├── write_savepoints_1.out │ ├── write_sqlalchemy.out │ ├── write_test.out │ ├── write_test_1.out │ ├── write_test_2.out │ └── write_test_3.out └── sql │ ├── disable_jit.include │ ├── import_sqlalchemy.sql │ ├── import_test.sql │ ├── multicorn_alchemy_test.sql │ ├── multicorn_cache_invalidation.sql │ ├── multicorn_column_options_test.sql │ ├── multicorn_error_test.sql │ ├── multicorn_logger_test.sql │ ├── multicorn_planner_test.sql │ ├── multicorn_regression_test.sql │ ├── multicorn_sequence_test.sql │ ├── multicorn_test_date.sql │ ├── multicorn_test_dict.sql │ ├── multicorn_test_list.sql │ ├── multicorn_test_sort.sql │ ├── write_batch_test.sql │ ├── write_filesystem.sql │ ├── write_savepoints.sql │ ├── write_sqlalchemy.sql │ └── write_test.sql └── test-common ├── disable_jit.include └── multicorn_testfilesystem.include /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" 3 | fi 4 | use flake 5 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run multicorn2 Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'main' 8 | 9 | jobs: 10 | run_tests: 11 | name: run tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cachix/install-nix-action@v26 17 | 18 | # Binary cache is maintained in update_cache.yml; see comments there for details. 19 | - uses: zombiezen/setup-nix-cache-action@v0.4.0 20 | with: 21 | substituters: s3://mfenniak-multicorn2-nix-cache?region=us-west-2 22 | trusted_public_keys: mfenniak-multicorn2-nix-cache:mldk0J26tzkCmqihQKOiyuAm1HAs2CHdIZC0ESr56ZA= 23 | 24 | # Seems to be a bug in setup-nix-cache-action where, even though we're only providing a trusted_public_keys input 25 | # and no secret_keys input, it still tries to create a post-build-hook that will push packages to the cache. This 26 | # is a hacky workaround to remove that hook -- we find the post-build-hook created by setup-nix-cache-action and 27 | # truncate it to 0 bytes so it does nothing. 28 | - run: sudo find /home/runner/work/_temp -type d -name 'setup-nix-cache-action-*' -exec sh -c 'truncate -s 0 "$1"/post-build-hook' _ {} \; 29 | 30 | # Run all the tests. 31 | - run: nix build .#allTestSuites 32 | -------------------------------------------------------------------------------- /.github/workflows/update_cache.yml: -------------------------------------------------------------------------------- 1 | name: Update build cache 2 | 3 | # PostgreSQL provided by nixpkgs doesn't include PL/Python support, so we have to build it ourselves (in flake.nix). In 4 | # fact, we have to build it once for every version of Python that we want to run tests against. That's pretty slow. 5 | # 6 | # In order to speed up the tests, we build all the supported versions of PostgreSQL with PL/Python support and push them 7 | # to a nix binary cache. 8 | # 9 | # However GitHub Actions (wisely) doesn't allow access to secrets in pull requests, so we can't push to the cache from 10 | # the test runs. Instead, we push to the cache from this workflow, which is triggered manually, and triggered on 11 | # changes to flake.nix & flake.lock in the main branch push. This should keep the cache up-to-date, but without running 12 | # untrusted code with access to the secrets. 13 | 14 | # Secrets required: 15 | # 16 | # AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY secrets are generated by creating a new AWS IAM user which has these 17 | # permissions, and generating an access key for the user: 18 | # { 19 | # "Version": "2012-10-17", 20 | # "Statement": [ 21 | # { 22 | # "Sid": "VisualEditor0", 23 | # "Effect": "Allow", 24 | # "Action": [ 25 | # "s3:PutObject", 26 | # "s3:GetObject", 27 | # "s3:ListBucket" 28 | # ], 29 | # "Resource": [ 30 | # "arn:aws:s3:::mfenniak-multicorn2-nix-cache/*", 31 | # "arn:aws:s3:::mfenniak-multicorn2-nix-cache" 32 | # ] 33 | # } 34 | # ] 35 | # } 36 | # 37 | # NIX_CACHE_SECRET_KEY secret is generated by: 38 | # nix key generate-secret --key-name mfenniak-multicorn2-nix-cache > ./secret 39 | # then "./secret" file is uploaded as secret NIX_CACHE_SECRET_KEY. The related public key is extracted by: 40 | # nix show-public-key --key-file ./secret 41 | # which then needs to be populated as the trusted_public_keys input to the setup-nix-cache-action in run_tests.yml. 42 | 43 | on: 44 | workflow_dispatch: 45 | push: 46 | branches: 47 | - 'main' 48 | paths: 49 | - 'flake.nix' 50 | - 'flake.lock' 51 | 52 | jobs: 53 | update_cache: 54 | name: Update allPostgresWithPlPython Cache 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: cachix/install-nix-action@v26 60 | - uses: zombiezen/setup-nix-cache-action@v0.4.0 61 | with: 62 | substituters: s3://mfenniak-multicorn2-nix-cache?region=us-west-2 63 | secret_keys: ${{ secrets.NIX_CACHE_SECRET_KEY }} 64 | aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} 65 | aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 66 | # allPostgresWithPlPython is the target that contains all the supported versions of PostgreSQL with PL/Python. 67 | # 68 | # setup-nix-cache-action will automatically copy the build result to the first substituter, so no need for a nix 69 | # copy after the build. 70 | - run: nix build .#allPostgresWithPlPython 71 | # Might as well build the test suites too and cache them -- while they're fast and not typically cacheable in a 72 | # meaningful way, they do rely on Python versions with packages (like SQLAlchemy) that will be effectively cached 73 | # by this. 74 | - run: nix build .#allTestSuites 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | setup--2.* 2 | *.o 3 | *.bc 4 | directories.stamp 5 | *.so 6 | build/ 7 | dist/ 8 | python/multicorn.egg-info/ 9 | results/ 10 | regression.* 11 | .direnv 12 | result -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 3.1: 2 | done so far: 3 | - opened up for development 4 | - drop support for Postgres versions prior to 13 5 | - confirm support for Python 3.12 6 | - confirm support for pg17 7 | still to do: 8 | - automated testing for pg17 9 | - confirm support for Python 3.12 10 | 11 | 12 | 13 | 3.0: 14 | - drop support for Python 3 versions prior to 3.9 15 | - drop support for Postgres versions prior to 12 16 | - PG14: fix DELETE to not passing rowid_column to the FDW (https://github.com/pgsql-io/multicorn2/pull/41) 17 | - fix bug in error handling on SQL_ASCII databases (https://github.com/pgsql-io/multicorn2/pull/43) 18 | - PG14: fix UPDATE to fetch all columns for consistency (https://github.com/pgsql-io/multicorn2/pull/48) 19 | - PG14: fix DELETE RETURNING providing NULL values (https://github.com/pgsql-io/multicorn2/pull/47) 20 | - Update SQLAlchemy FDW to be tested against SQLAlchemy 2.0; earlier versions not supported but may work (https://github.com/pgsql-io/multicorn2/pull/49) 21 | - PG15: now supported with full regression test passes; note that PG15 will run FDW rollback handlers at slightly different times during error handling (https://github.com/pgsql-io/multicorn2/pull/50) 22 | - Add support for bulk_insert FDWs on PG14+ (https://github.com/pgsql-io/multicorn2/pull/45) 23 | - PG16: Fix compatibility issues w/ log_to_postgres and join query planning in PostgreSQL 16 (https://github.com/pgsql-io/multicorn2/pull/51) 24 | - Fix crashes in EXPLAIN with complex quals (https://github.com/pgsql-io/multicorn2/pull/54) 25 | - Support Python 3.11 (https://github.com/pgsql-io/multicorn2/pull/59) 26 | - Behavior change: When log_to_postgres with level ERROR or FATAL is invoked, a specialized Python exception will be thrown and the stack unwound, allowing `catch` and `finally` blocks, and other things like context handler exits, to be invoked in the FDW. (https://github.com/pgsql-io/multicorn2/pull/59) 27 | 28 | 2.0: 29 | - drop support for Postgres versions prior to 10 30 | - drop support for Python 2 31 | - support for Python 3.6++ 32 | - add support for PG13 33 | - experimental support for PG14 34 | 35 | 1.4.0: 36 | - Lots of maintenance done by Kamil Gałuszka 37 | - Add compatibility with PostgreSQL 11 / 12 (Jeff Janes, Dmitry Bogatov) 38 | - Add compatibility with Python 3.8 (Stefano Rivera) 39 | - Fix for mishandling of None elements in sequence lines. (Lance Cooper) 40 | - Lots of bug fixes (Dmitry Bogatov) 41 | - Import views along with tables (Richard Gibson) 42 | - Expand the scope and capability of column type conversion (Richard Gibson) 43 | - Fix SQLServer TinyInt mapping (Rastko Karadzic) 44 | - Fix build on MacOS (Larry Siden) 45 | - Add Docker images (Dmitry Bogatov) 46 | 47 | 1.3.4: 48 | - Add compatibility with PostgreSQL 10 49 | 50 | 1.3.3: 51 | - Add compatibility with PostgreSQL 9.6 52 | - Fix bug with typecasting of params 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2025, Lussier 2 | Portions Copyright (c) 2013, Kozea 3 | 4 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. 5 | 6 | IN NO EVENT SHALL LUSSIER or KOZEA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF KOZEA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 7 | 8 | LUSSIER & KOZEA SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND KOZEA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 9 | -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multicorn", 3 | "abstract": "Multicorn2 Python bindings for Postgres 10+ Foreign Data Wrapper", 4 | "description": "The Multicorn2 Foreign Data Wrapper allows you to write extensions in python.", 5 | "version": "__VERSION__", 6 | "maintainer": "Denis Lussier ", 7 | "license": { 8 | "PostgreSQL": "http://www.postgresql.org/about/licence" 9 | }, 10 | "resources": { 11 | "repository": { 12 | "url": "git://github.com/pgsql-io/multicorn2.git", 13 | "type": "git" 14 | }, 15 | "homepage": "http://multicorn2.org", 16 | "bugtracker": { 17 | "web": "https://github.com/pgsql-io/multicorn2", 18 | "mailto": "denis@lussier.io" 19 | } 20 | }, 21 | "provides": { 22 | "multicorn": { 23 | "abstract": "Multicorn Python bindings for Postgres 13+ Foreign Data Wrapper", 24 | "file": "sql/multicorn.sql", 25 | "docfile": "doc/multicorn.md", 26 | "version": "__VERSION__" 27 | } 28 | }, 29 | "prereqs": { 30 | "runtime": { 31 | "requires": { 32 | "PostgreSQL": "13.0", 33 | "Python": "3.9.3" 34 | } 35 | } 36 | }, 37 | 38 | "release_status": "stable", 39 | "generated_by": "Lussier", 40 | "meta-spec": { 41 | "version": "3.1", 42 | "url": "http://pgxn.org/meta/spec.txt" 43 | }, 44 | "tags": [ 45 | "multicorn", 46 | "python", 47 | "csv", 48 | "ldap", 49 | "imap", 50 | "filesystem", 51 | "SQL MED", 52 | "foreign data wrapper", 53 | "fdw", 54 | "external data" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | srcdir = . 2 | MODULE_big = multicorn 3 | OBJS = src/errors.o src/python.o src/query.o src/multicorn.o 4 | 5 | 6 | DATA = $(filter-out $(wildcard sql/*--*.sql),$(wildcard sql/*.sql)) 7 | 8 | DOCS = $(wildcard $(srcdir)/doc/*.md) 9 | 10 | EXTENSION = multicorn 11 | EXTVERSION = $(shell grep default_version $(srcdir)/$(EXTENSION).control | sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") 12 | 13 | all: sql/$(EXTENSION)--$(EXTVERSION).sql 14 | 15 | directories.stamp: 16 | [ -d sql ] || mkdir sql 17 | [ -d src ] || mkdir src 18 | touch $@ 19 | 20 | $(OBJS): directories.stamp 21 | 22 | install: python_code 23 | 24 | sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql directories.stamp 25 | cp $< $@ 26 | 27 | preflight-check: 28 | $(srcdir)/preflight-check.sh 29 | 30 | python_code: setup.py pyproject.toml 31 | $(eval python_major_version := $(shell echo ${python_version} | cut -d '.' -f 1)) 32 | $(eval PIP ?= $(shell ([ -x "$$(command -v pip${python_version})" ] && echo pip${python_version}) || ([ -x "$$(command -v pip${python_major_version})" ] && echo pip${python_major_version}) || echo pip)) 33 | # 34 | # Strictly speaking, --break-system-packages arrived in 23.0.1, but that's 35 | # too hard to check for. 36 | # 37 | $(eval PIP_VERSION := $(shell $(PIP) --version 2>&1 | cut -d ' ' -f 2 | cut -d '.' -f 1)) 38 | $(eval PIP_FLAGS := $(shell [ $(PIP_VERSION) -ge 23 ] && echo "--break-system-packages --ignore-installed")) 39 | # 40 | # Workaround https://github.com/pgsql-io/multicorn2/issues/34, and then 41 | # re-evaluate PIP_VERSION/PIP_FLAGS. 42 | # 43 | $(PIP) install $(PIP_FLAGS) --upgrade 'pip>=23' 44 | $(eval PIP_VERSION := $(shell $(PIP) --version 2>&1 | cut -d ' ' -f 2 | cut -d '.' -f 1)) 45 | $(eval PIP_FLAGS := $(shell [ $(PIP_VERSION) -ge 23 ] && echo "--break-system-packages --ignore-installed")) 46 | $(PIP) install $(PIP_FLAGS) . 47 | 48 | release-zip: all 49 | git archive --format zip --prefix=multicorn-$(EXTVERSION)/ --output ./multicorn-$(EXTVERSION).zip HEAD 50 | unzip ./multicorn-$(EXTVERSION).zip 51 | rm ./multicorn-$(EXTVERSION).zip 52 | sed -i -e "s/__VERSION__/$(EXTVERSION)/g" ./multicorn-$(EXTVERSION)/META.json ./multicorn-$(EXTVERSION)/python/multicorn/__init__.py 53 | zip -r ./multicorn-$(EXTVERSION).zip ./multicorn-$(EXTVERSION)/ 54 | rm ./multicorn-$(EXTVERSION) -rf 55 | 56 | coverage: 57 | lcov -d . -c -o lcov.info --no-external 58 | genhtml --show-details --legend --output-directory=coverage --title="Multicorn Code Coverage" --no-branch-coverage --num-spaces=4 --prefix=./src/ `find . -name lcov.info -print` 59 | 60 | DATA = sql/$(EXTENSION)--$(EXTVERSION).sql 61 | EXTRA_CLEAN = sql/$(EXTENSION)--$(EXTVERSION).sql ./multicorn-$(EXTVERSION).zip directories.stamp 62 | PG_CONFIG ?= pg_config 63 | PGXS := $(shell $(PG_CONFIG) --pgxs) 64 | REGRESS = virtual_tests 65 | 66 | include $(PGXS) 67 | 68 | with_python_no_override = no 69 | 70 | ifeq ($(with_python),yes) 71 | with_python_no_override = yes 72 | endif 73 | 74 | ifdef PYTHON_OVERRIDE 75 | with_python_no_override = no 76 | endif 77 | 78 | 79 | ifeq ($(with_python_no_override),yes) 80 | SHLIB_LINK = $(python_libspec) $(python_additional_libs) $(filter -lintl,$(LIBS)) 81 | override CPPFLAGS := -I. -I$(srcdir) $(python_includespec) $(CPPFLAGS) 82 | override PYTHON = python${python_version} 83 | else 84 | ifdef PYTHON_OVERRIDE 85 | override PYTHON = ${PYTHON_OVERRIDE} 86 | endif 87 | 88 | ifeq (${PYTHON}, ) 89 | override PYTHON = python 90 | endif 91 | 92 | 93 | python_version = $(shell ${PYTHON} --version 2>&1 | cut -d ' ' -f 2 | cut -d '.' -f 1-2) 94 | PYTHON_CONFIG ?= python${python_version}-config 95 | 96 | PY_LIBSPEC = $(shell ${PYTHON_CONFIG} --embed >/dev/null && ${PYTHON_CONFIG} --libs --embed || ${PYTHON_CONFIG} --libs) 97 | PY_INCLUDESPEC = $(shell ${PYTHON_CONFIG} --includes) 98 | PY_CFLAGS = $(shell ${PYTHON_CONFIG} --cflags) 99 | PY_LDFLAGS = $(shell ${PYTHON_CONFIG} --ldflags) 100 | SHLIB_LINK += $(PY_LIBSPEC) $(PY_LDFLAGS) $(PY_ADDITIONAL_LIBS) $(filter -lintl,$(LIBS)) 101 | override PG_CPPFLAGS := $(PY_INCLUDESPEC) $(PG_CPPFLAGS) 102 | override CPPFLAGS := $(PG_CPPFLAGS) $(CPPFLAGS) 103 | endif 104 | 105 | ifeq ($(PORTNAME),Darwin) 106 | override LDFLAGS += -undefined dynamic_lookup -bundle_loader $(shell $(PG_CONFIG) --bindir)/postgres 107 | endif 108 | 109 | PYTHON_TEST_VERSION ?= $(python_version) 110 | PG_TEST_VERSION ?= $(MAJORVERSION) 111 | UNSUPPORTS_SQLALCHEMY=$(shell python -c "import sqlalchemy;import psycopg2" 1> /dev/null 2>&1; echo $$?) 112 | 113 | TESTS = test-$(PYTHON_TEST_VERSION)/sql/multicorn_cache_invalidation.sql \ 114 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_column_options_test.sql \ 115 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_error_test.sql \ 116 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_logger_test.sql \ 117 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_planner_test.sql \ 118 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_regression_test.sql \ 119 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_sequence_test.sql \ 120 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_date.sql \ 121 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_dict.sql \ 122 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_list.sql \ 123 | test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_sort.sql \ 124 | test-$(PYTHON_TEST_VERSION)/sql/write_filesystem.sql \ 125 | test-$(PYTHON_TEST_VERSION)/sql/write_savepoints.sql \ 126 | test-$(PYTHON_TEST_VERSION)/sql/write_test.sql \ 127 | test-$(PYTHON_TEST_VERSION)/sql/import_test.sql 128 | 129 | ifeq (${UNSUPPORTS_SQLALCHEMY}, 0) 130 | TESTS += test-$(PYTHON_TEST_VERSION)/sql/multicorn_alchemy_test.sql \ 131 | test-$(PYTHON_TEST_VERSION)/sql/write_sqlalchemy.sql \ 132 | test-$(PYTHON_TEST_VERSION)/sql/import_sqlalchemy.sql 133 | endif 134 | 135 | # Add tests for features only supported in PG14+ 136 | ifeq ($(shell test $(PG_TEST_VERSION) -ge 14; echo $$?),0) 137 | TESTS += test-$(PYTHON_TEST_VERSION)/sql/write_batch_test.sql 138 | endif 139 | 140 | REGRESS = $(patsubst test-$(PYTHON_TEST_VERSION)/sql/%.sql,%,$(TESTS)) 141 | REGRESS_OPTS = --inputdir=test-$(PYTHON_TEST_VERSION) --encoding=UTF8 --host=localhost 142 | 143 | $(info Python version is $(python_version)) 144 | 145 | # This is a copy of the "check" target from pgxs.mk, but it doesn't build the extension, just runs the tests. 146 | easycheck: 147 | $(pg_regress_check) $(REGRESS_OPTS) $(REGRESS) 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Multicorn2 3 | ========== 4 | 5 | Multicorn Python3 Foreign Data Wrapper (FDW) for Postgresql. Tested on Linux w/ Python 3.9-3.12 & Postgres 13-17. 6 | 7 | Testing is underway for supporting Python 3.13 and is expected in v3.1. Newest versions of major linux distro's (Debian 12, Ubuntu 24.04 & EL10) are all still using Python 3.12 so sticking with using 3.12 is advised in the short run. 8 | 9 | The Multicorn Foreign Data Wrapper allows you to fetch foreign data in Python in your PostgreSQL server. 10 | 11 | Multicorn2 is distributed under the PostgreSQL license. See the LICENSE file for 12 | details. 13 | 14 | ## How it works 15 | 16 | In use, Multicorn includes a Python package which contains: 17 | 18 | - A `__init__.py` which provides a base class from which your new, 19 | custom, Extension will derive. 20 | - A `utils.py` containing diagnostic support code your Extension can use. 21 | - Several useful example FDW implementations. 22 | 23 | Multicorn also includes, under the covers, **two** shared libraries: 24 | 25 | - `multicorn.so` contains a generic Postgres FDW extension which 26 | interfaces between Postgres and your custom FDW. 27 | - `_utils.so` is a CPython extension which provides support for 28 | the previously mentioned `utils.py`. 29 | 30 | ## Using in PGEDGE (see https://github.com/pgedge/pgedge) 31 | 32 | 1.) Install pgEdge from the command line, in your home directory, with the curl command at the top of pgedge/pgedge 33 | 34 | 2.) Change into the pgedge directory and install pgXX 35 | ```bash 36 | cd pgedge 37 | ./pgedge install pg17 --start 38 | ./pgedge install multicorn2 39 | ``` 40 | 41 | 3.) Use multicorn as you normally would AND you can install popular FDW's that use multicorn such as ElasticSerachFDW & BigQueryFDW 42 | ```bash 43 | ./pgedge install mqttclient 44 | ``` 45 | 46 | ## Building Multicorn2 against Postgres from Source 47 | 48 | It is built the same way all standard postgres extensions are built with following dependcies needing to be installed: 49 | 50 | ### Install Dependencies for Building Postgres & the Multicorn2 extension 51 | On Debian/Ubuntu systems: 52 | ```bash 53 | sudo apt install -y build-essential libreadline-dev zlib1g-dev flex bison libxml2-dev libxslt-dev libssl-dev libxml2-utils xsltproc 54 | sudo apt install -y python3 python3-dev python3-setuptools python3-pip 55 | ``` 56 | 57 | On CentOS/Rocky/Redhat systems: 58 | ```bash 59 | sudo yum install -y bison-devel readline-devel libedit-devel zlib-devel openssl-devel bzip2-devel libmxl2 libxslt-devel wget 60 | sudo yum groupinstall -y 'Development Tools' 61 | 62 | sudo yum -y install git python3 python3-devel python3-pip 63 | ``` 64 | 65 | ### Upgrade to latest PIP (recommended) 66 | ```bash 67 | cd ~ 68 | python3 -m venv venv 69 | source venv/bin/activate 70 | ``` 71 | 72 | ### Download & Compile Postgres 13+ source code 73 | ```bash 74 | cd ~ 75 | wget https://ftp.postgresql.org/pub/source/v17.0/postgresql-17.0.tar.gz 76 | tar -xvf postgresql-17.0.tar.gz 77 | cd postgresql-17.0 78 | ./configure 79 | make 80 | sudo make install 81 | ``` 82 | 83 | ### Download & Compile Multicorn2 84 | ```bash 85 | set PATH=/usr/local/pgsql/bin:$PATH 86 | cd ~/postgresql-17.0/contrib 87 | wget https://github.com/pgsql-io/multicorn2/archive/refs/tags/v3.0.tar.gz 88 | tar -xvf v3.0.tar.gz 89 | cd multicorn2-3.0 90 | make 91 | sudo make install 92 | ``` 93 | 94 | ### Create Multicorn2 Extension 95 | In your running instance of Postgres from the PSQL command line 96 | ```sql 97 | CREATE EXTENSION multicorn; 98 | ``` 99 | 100 | ## Building Multicorn2 against pre-built Postgres 101 | 102 | When using a pre-built Postgres installed using your OS package manager, you will need the additional package that enables Postgres extensions to be built: 103 | 104 | ### Install Dependencies for Building the Multicorn2 extension 105 | On Debian/Ubuntu systems: 106 | ```bash 107 | sudo apt install -y build-essential ... postgresql-server-dev-13 108 | sudo apt install -y python3 python3-dev python3-setuptools python3-pip 109 | ``` 110 | 111 | On CentOS/Rocky/Redhat systems: 112 | ```bash 113 | sudo yum install -y ... 114 | sudo yum groupinstall -y 'Development Tools' 115 | sudo yum -y install git python3 python3-devel python3-pip 116 | ``` 117 | 118 | ### Download & Compile Multicorn2 119 | ```bash 120 | wget https://github.com/pgsql-io/multicorn2/archive/refs/tags/v3.0.tar.gz 121 | tar -xvf v3.0.tar.gz 122 | cd multicorn2-3.0 123 | make 124 | sudo make install 125 | ``` 126 | 127 | Note that the last step installs both the extension into Postgres and also the Python part. The latter is done using "pip install", and so can be undone using "pip uninstall". 128 | 129 | ### Create Multicorn2 Extension 130 | In your running instance of Postgres from the PSQL command line 131 | ```sql 132 | CREATE EXTENSION multicorn; 133 | ``` 134 | 135 | ## Integration tests 136 | 137 | multicorn2 has an extensive suite of integration tests which run against live PostgreSQL servers. In order to manage the matrix of supported versions of Python and PostgreSQL, the Nix package manager can be used to provide all the dependencies and run all the tests. The Nix package manager is supported on Linux, MacOS, and Windows Subsystem for Linux. To install Nix, follow the instructions at https://nixos.org/download/. 138 | 139 | Once Nix is installed, you can run the tests with the following commands. To run a complete regression test against all supported versions of Python and PostgreSQL, run: 140 | 141 | ```bash 142 | nix build .#allTestSuites 143 | ``` 144 | 145 | This can be slow to run when it is first executed (15-30 minutes) due to the need to rebuild PostgreSQL with specific versions of Python, to support the plpython extension which is used in some tests. Subsequent runs will be faster (under 5 minutes) as the build artifacts are cached. 146 | 147 | To run a faster test suite against a specific version of Python and PostgreSQL, run: 148 | 149 | ```bash 150 | nix build .#testSuites.test_pg13_py39 151 | ``` 152 | 153 | **Known issues:** 154 | - The tests cover the supported range of Python & PostgreSQL combinations; 155 | 156 | ### Adding new Python or PostgreSQL versions to the test suite 157 | 158 | 1. Perform a `nix flake update` in order to provide access to the latest packages available from the Nix package manager. 159 | 160 | 2. Update the supported list of versions in flake.nix under the variable `testPythonVersions` or `testPostgresVersions`. 161 | 162 | 3. For new Python versions, create a new symlink to test-3.9 with the version number of the new Python version; and update the list of test directories in `makeTestSuite` in flake.nix. 163 | 164 | 4. Run the tests with `nix build .#allTestSuites` to ensure that the new versions are supported. 165 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Multicorn.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Multicorn.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Multicorn" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Multicorn" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .api_compatibility { 2 | line-height: 1.3em; 3 | } 4 | 5 | .api_compatibility > i { 6 | margin-left: 1em; 7 | text-opacity: 0; 8 | width: 40px; 9 | height: 32px; 10 | padding-left: 16px; 11 | padding-right: 16px; 12 | padding-top: 16px; 13 | padding-bottom: 16px; 14 | font-size: 2em; 15 | text-align: right; 16 | background-repeat: no-repeat; 17 | background-position: left, center; 18 | } 19 | 20 | .api_compatibility > i.checked { 21 | color: green; 22 | } 23 | 24 | .api_compatibility > i.unchecked { 25 | color: red; 26 | } 27 | 28 | .compat-read { 29 | background-image: url('../img/glyphicons-352-book-open.png'); 30 | } 31 | 32 | .compat-write { 33 | background-image: url('../img/glyphicons-151-edit.png'); 34 | } 35 | 36 | .compat-transaction { 37 | background-image: url('../img/glyphicons-142-database-plus.png'); 38 | } 39 | 40 | .compat-import_schema { 41 | background-image: url('../img/glyphicons-419-disk-import.png'); 42 | } 43 | -------------------------------------------------------------------------------- /doc/_static/img/glyphicons-142-database-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/doc/_static/img/glyphicons-142-database-plus.png -------------------------------------------------------------------------------- /doc/_static/img/glyphicons-151-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/doc/_static/img/glyphicons-151-edit.png -------------------------------------------------------------------------------- /doc/_static/img/glyphicons-352-book-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/doc/_static/img/glyphicons-352-book-open.png -------------------------------------------------------------------------------- /doc/_static/img/glyphicons-419-disk-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/doc/_static/img/glyphicons-419-disk-import.png -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | *** 2 | API 3 | *** 4 | 5 | The API is split into two modules: the ``multicorn`` module and the 6 | `utils` module: 7 | 8 | - The ``multicorn`` module contains the whole API needed for implementing 9 | a Foreign Data Wrapper. 10 | - The ``utils`` module contains logging and error reporting functions, 11 | which are ultimately implemented as calls to the PostgreSQL API. 12 | 13 | 14 | Implementing an FDW 15 | =================== 16 | 17 | Implementing an FDW is as simple as implementing the 18 | :py:class:`~multicorn.ForeignDataWrapper` class. 19 | 20 | 21 | 22 | Required API 23 | ------------ 24 | 25 | .. py:currentmodule:: multicorn.ForeignDataWrapper 26 | 27 | This subset of the API allows your ForeignDataWrapper to be used for read-only 28 | queries. 29 | 30 | You have to implement the following methods: 31 | 32 | - :py:meth:`__init__` 33 | - :py:meth:`execute` 34 | 35 | .. note:: In the documentation, FDWs implementing this API will be marked with: 36 | 37 | .. api_compat:: :read: 38 | 39 | 40 | Write API 41 | --------- 42 | 43 | To implement full write capabilites, the following property must be implemented: 44 | 45 | 46 | .. autoattribute:: multicorn.ForeignDataWrapper.rowid_column 47 | 48 | In addition to that, you should implement each DML operation as you see fit: 49 | 50 | - :py:meth:`insert` 51 | - :py:meth:`update` 52 | - :py:meth:`delete` 53 | 54 | 55 | 56 | .. note:: In the documentation, FDWs implementing this API will be marked with: 57 | 58 | .. api_compat:: 59 | :write: 60 | 61 | Transactional API 62 | ----------------- 63 | 64 | Transactional Capabilities can be implemented with the following methods: 65 | 66 | 67 | .. automethod:: multicorn.ForeignDataWrapper.begin 68 | .. automethod:: multicorn.ForeignDataWrapper.pre_commit 69 | .. automethod:: multicorn.ForeignDataWrapper.rollback 70 | .. automethod:: multicorn.ForeignDataWrapper.sub_begin 71 | .. automethod:: multicorn.ForeignDataWrapper.sub_commit 72 | .. automethod:: multicorn.ForeignDataWrapper.sub_rollback 73 | 74 | .. note:: In the documentation, FDWs implementing this API will be marked with: 75 | 76 | .. api_compat:: 77 | :transaction: 78 | 79 | 80 | Full API 81 | ======== 82 | 83 | .. autoclass:: multicorn.ForeignDataWrapper 84 | :special-members: __init__ 85 | :members: 86 | 87 | 88 | .. autoclass:: multicorn.SortKey 89 | 90 | .. autoclass:: multicorn.Qual 91 | :members: 92 | 93 | .. autoclass:: multicorn.ColumnDefinition 94 | :members: 95 | 96 | .. autoclass:: multicorn.TableDefinition 97 | :members: 98 | 99 | 100 | -------------------------------------------------------------------------------- /doc/contribute.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Contribute 3 | ************ 4 | 5 | If you use Multicorn2 in production, we would love to hear about your use-case ! 6 | 7 | 8 | Report Bugs 9 | =========== 10 | 11 | Found a bug? Want a new feature? Report a new issue on the `Multicorn2 12 | bug-tracker on GitHub `. 13 | 14 | 15 | Hack 16 | ==== 17 | 18 | Interested in hacking? Feel free to clone the `git repository on 19 | GitHub ` if you want to add new features, fix bugs or update documentation. 20 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers.rst: -------------------------------------------------------------------------------- 1 | ****************************** 2 | Included Foreign Data Wrappers 3 | ****************************** 4 | 5 | Multicorn is bundled with a small set of Foreign Data Wrappers, which you can 6 | use or customize for your needs. 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | /foreign-data-wrappers/sqlalchemyfdw.rst 13 | /foreign-data-wrappers/fsfdw.rst 14 | /foreign-data-wrappers/imapfdw.rst 15 | /foreign-data-wrappers/ldapfdw.rst 16 | /foreign-data-wrappers/csvfdw.rst 17 | /foreign-data-wrappers/rssfdw.rst 18 | /foreign-data-wrappers/processfdw.rst 19 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/csvfdw.rst: -------------------------------------------------------------------------------- 1 | CSV Foreign Data Wrapper 2 | ************************ 3 | 4 | .. automodule:: multicorn.csvfdw 5 | :synopsis: 6 | 7 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/fsfdw.rst: -------------------------------------------------------------------------------- 1 | FileSystem Foreign Data Wrapper 2 | ******************************* 3 | 4 | .. automodule:: multicorn.fsfdw 5 | :synopsis: 6 | 7 | ReStructuredText FDW 8 | ~~~~~~~~~~~~~~~~~~~~ 9 | 10 | .. automodule:: multicorn.fsfdw.restfsfdw 11 | 12 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/imapfdw.rst: -------------------------------------------------------------------------------- 1 | Imap Foreign Data Wrapper 2 | ************************* 3 | 4 | .. automodule:: multicorn.imapfdw 5 | :synopsis: 6 | 7 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/ldapfdw.rst: -------------------------------------------------------------------------------- 1 | LDAP Foreign Data Wrapper 2 | ************************* 3 | 4 | .. automodule:: multicorn.ldapfdw 5 | :synopsis: 6 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/processfdw.rst: -------------------------------------------------------------------------------- 1 | Process Foreign Data Wrapper 2 | **************************** 3 | 4 | .. automodule:: multicorn.processfdw 5 | :synopsis: 6 | 7 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/rssfdw.rst: -------------------------------------------------------------------------------- 1 | RSS foreign data wrapper 2 | ************************ 3 | 4 | .. automodule:: multicorn.rssfdw 5 | :synopsis: 6 | 7 | -------------------------------------------------------------------------------- /doc/foreign-data-wrappers/sqlalchemyfdw.rst: -------------------------------------------------------------------------------- 1 | SQLAlchemy Foreign Data Wrapper 2 | ******************************* 3 | 4 | .. automodule:: multicorn.sqlalchemyfdw 5 | :synopsis: 6 | 7 | -------------------------------------------------------------------------------- /doc/getting-started.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Usage 3 | ***** 4 | 5 | The multicorn2 foreign data wrapper is not different from other foreign data 6 | wrappers. 7 | 8 | To use it, you have to: 9 | 10 | - Create the extension in the target database. 11 | As a PostgreSQL super user, run the following SQL: 12 | 13 | .. code-block:: sql 14 | 15 | CREATE EXTENSION multicorn; 16 | 17 | 18 | - Create a server. 19 | In the SQL ``OPTIONS`` clause, you must provide an options named wrapper, 20 | containing the fully-qualified class name of the concrete python foreign data 21 | wrapper you want to use: 22 | 23 | .. code-block:: sql 24 | 25 | CREATE SERVER multicorn_imap FOREIGN DATA WRAPPER multicorn 26 | options ( 27 | wrapper 'multicorn.imapfdw.ImapFdw' 28 | ); 29 | 30 | 31 | You can then proceed on with the actual foreign tables creation, and pass them 32 | the needed options. 33 | 34 | Each foreign data wrapper supports its own set of options, and may interpret the 35 | columns definitions differently. 36 | 37 | You should look at the documentation for the specific :doc:`Foreign Data Wraper documentation ` 38 | -------------------------------------------------------------------------------- /doc/implementing-an-fdw.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | Writing an FDW 3 | ************** 4 | 5 | If you want to write an FDW, we recommend you start with the 6 | :ref:`tutorial`. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | api.rst 12 | implementing-tutorial.rst 13 | 14 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Multicorn documentation master file, created by 2 | sphinx-quickstart on Thu Dec 4 08:56:10 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | Multicorn2 8 | ########## 9 | 10 | Multicorn2 is a PostgreSQL 10++ extension meant to make `Foreign Data Wrapper`_ 11 | development easy, by allowing the programmer to use the Python programming 12 | language. 13 | 14 | If you just wanto use it as soon as possible, jump straight to the 15 | `installation` section. 16 | 17 | 18 | Contents: 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | installation.rst 24 | getting-started.rst 25 | foreign-data-wrappers.rst 26 | implementing-an-fdw.rst 27 | third-party-fdw.rst 28 | internals.rst 29 | contribute.rst 30 | 31 | 32 | Indices and tables 33 | ################## 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | 39 | 40 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Installation 3 | ************ 4 | 5 | Requirements 6 | ============ 7 | 8 | - Postgresql 10++ 9 | - Postgresql development packages 10 | - Python development packages 11 | - python >= 3.6 as your default python 12 | 13 | With the `pgxn client`_:: 14 | 15 | pgxn install multicorn 16 | 17 | From pgxn: 18 | 19 | .. parsed-literal:: 20 | 21 | wget |multicorn_pgxn_download| 22 | unzip multicorn-|multicorn_release| 23 | cd multicorn-|multicorn_release| 24 | make && sudo make install 25 | 26 | From source:: 27 | 28 | git clone git://github.com/pgsql-io/Multicorn2.git 29 | cd Multicorn2 30 | make && make install 31 | 32 | .. _pgxn client: http://pgxnclient.projects.postgresql.org/ 33 | 34 | -------------------------------------------------------------------------------- /doc/internals.rst: -------------------------------------------------------------------------------- 1 | ************************* 2 | Multicorn Internal Design 3 | ************************* 4 | 5 | This part is more geared toward those who may want to hack on Multicorn itself. 6 | 7 | 8 | PostgreSQL C API 9 | ================ 10 | 11 | 12 | The PostgreSQL C API follows a pretty simple workflow. 13 | 14 | 15 | -------------------------------------------------------------------------------- /doc/multicorn.md: -------------------------------------------------------------------------------- 1 | Multicorn2 2 | ========== 3 | 4 | Synopsis 5 | -------- 6 | 7 | Multicorn2 is a PostgreSQL 10++ extension allowing to write Foreign Data Wrappers 8 | in python. 9 | It is bundled with some foreign data wrappers. 10 | 11 | 12 | Usage 13 | ----- 14 | 15 | Create the extension (as a super user, on your target database): 16 | 17 | 18 | CREATE EXTENSION multicorn; 19 | 20 | 21 | 22 | Define a foreign server for the specific python foreign data wrappers you want 23 | to use: 24 | 25 | 26 | 27 | CREATE SERVER my_server_name FOREIGN DATA WRAPPER multicorn 28 | options ( 29 | wrapper 'python.class.Name' 30 | ) 31 | 32 | 33 | Where *python.class.Name* is a string defining which foreign data wrapper class 34 | to use. 35 | 36 | Ex, for the Imap foreign data wrapper: 37 | 38 | 39 | 40 | CREATE SERVER multicorn_imap FOREIGN DATA WRAPPER multicorn 41 | options ( 42 | wrapper 'multicorn.imapfdw.ImapFdw' 43 | ); 44 | 45 | 46 | Once you have a server set up, you can create foreign tables on your server. 47 | 48 | The foreign table must be supplied its required options. 49 | 50 | Ex: 51 | 52 | 53 | 54 | create foreign table gmail ( 55 | "Message-ID" character varying, 56 | "From" character varying, 57 | "Subject" character varying, 58 | "payload" character varying, 59 | "flags" character varying[], 60 | "To" character varying) server multicorn_imap options ( 61 | host 'imap.gmail.com', 62 | port '465', 63 | payload_column 'payload', 64 | flags_column 'flags', 65 | ssl 'True', 66 | login 'mylogin', 67 | password 'mypassword' 68 | ); 69 | -------------------------------------------------------------------------------- /doc/multicorn_directives/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from docutils.parsers.rst import Directive 3 | from docutils.parsers.rst import directives 4 | from docutils.nodes import Element 5 | from sphinx.builders.html import StandaloneHTMLBuilder 6 | 7 | 8 | class api_compat(Element): 9 | 10 | def __init__(self, api=None): 11 | self.api = api or {} 12 | super(api_compat, self).__init__() 13 | 14 | def visit_api_compat_node_html(self, node): 15 | self.body.append(u'Supports: %s' % "".join( 16 | [u'%s' % 17 | (key, "checked" if val else "unchecked", key, 18 | u"✓" if val else u"✗") 19 | for (key, val) in node.api.items()])) 20 | 21 | 22 | def depart_api_compat_node_html(self, node): 23 | self.body.append("") 24 | 25 | 26 | def visit_api_compat_node_text(self, node): 27 | self.add_text("Supported API: %s" % ",".join(node.api)) 28 | 29 | 30 | def depart_api_compat_node_text(self, node): 31 | pass 32 | 33 | 34 | def visit_api_compat_node_latex(self, node): 35 | # TODO: make it render in latex 36 | classes = node.get('classes', []) 37 | self.body.append('\n\\begin{notice}\n') 38 | self.body.append("Supported API: %s" % ",".join(node.api)) 39 | 40 | 41 | 42 | def depart_api_compat_node_latex(self, node): 43 | self.body.append('\\end{notice}\n') 44 | 45 | 46 | def setup(app): 47 | app.add_directive('api_compat', APICompatDirective) 48 | app.add_node(api_compat, 49 | html=(visit_api_compat_node_html, depart_api_compat_node_html), 50 | latex=(visit_api_compat_node_latex, depart_api_compat_node_latex), 51 | text=(visit_api_compat_node_text, depart_api_compat_node_text)) 52 | 53 | 54 | class APICompatDirective(Directive): 55 | 56 | has_content = True 57 | 58 | option_spec = { 59 | 'read': directives.flag, 60 | 'write': directives.flag, 61 | 'transaction': directives.flag, 62 | 'import_schema': directives.flag 63 | } 64 | 65 | def run(self): 66 | values = {key: key in self.options 67 | for key in self.option_spec} 68 | return [api_compat(api=values)] 69 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-napoleon 3 | mock 4 | sqlalchemy 5 | funcsigs 6 | -------------------------------------------------------------------------------- /doc/third-party-fdw.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Third-Party-FDWs 3 | **************** 4 | 5 | In addition to the built-in fdws shipped with Multicorn, there are some 6 | third-party modules available on the net. 7 | 8 | 9 | rethinkdb-multicorn-postgresql-fdw 10 | ================================== 11 | 12 | 13 | A FDW for accessing RethinkDB databases 14 | 15 | .. api_compat:: 16 | :read: 17 | :write: 18 | 19 | repository: 20 | https://github.com/wilsonrmsorg/rethinkdb-multicorn-postgresql-fdw/ 21 | 22 | 23 | Hive FDW 24 | ======== 25 | 26 | Access data stored in Apache Hive tables. 27 | 28 | .. api_compat:: :read: 29 | 30 | repository: 31 | https://github.com/youngwookim/hive-fdw-for-postgresql 32 | 33 | 34 | dockerfdw 35 | ========= 36 | 37 | 38 | A FDW for interacting with docker containers 39 | 40 | .. api_compat:: 41 | :read: 42 | :write: 43 | 44 | repository 45 | https://github.com/paultag/dockerfdw 46 | 47 | fb-psql 48 | ======= 49 | 50 | Access data using Facebook FQL API 51 | 52 | .. api_compat:: 53 | :read: 54 | 55 | repository 56 | https://github.com/mrwilson/fb-psql 57 | 58 | telemetry-fdw 59 | ============= 60 | 61 | Reads data from OpenStack / Telemetry. 62 | 63 | .. api_compat:: :read: 64 | 65 | repository 66 | https://github.com/hhamalai/telemetry-fdw 67 | 68 | 69 | s3csv_fdw 70 | ========= 71 | 72 | Reads data from CSV files stored on Amazon S3. 73 | 74 | .. api_compat:: :read: 75 | 76 | repository 77 | https://github.com/eligoenergy/s3csv_fdw.git 78 | 79 | 80 | S3Fdw 81 | ===== 82 | 83 | Reads data from JSON files stored on Amazon S3 84 | 85 | .. api_compat:: :read: 86 | 87 | repository 88 | https://github.com/blakedw/s3fdw.git 89 | 90 | 91 | Database.com FDW 92 | ================ 93 | 94 | .. api_compat:: :read: 95 | 96 | repository 97 | https://github.com/metadaddy-sfdc/Database.com-FDW-for-PostgreSQL.git 98 | 99 | 100 | pgCassandra FDW 101 | ================ 102 | 103 | .. api_compat:: :read: 104 | 105 | repository 106 | https://github.com/wjch-krl/pgCassandra.git 107 | -------------------------------------------------------------------------------- /docheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | usage() { 5 | cat << EOF 6 | Usage: ./docheck command [args] 7 | 8 | Available commands: 9 | 10 | EOF 11 | } 12 | 13 | if [[ $# = 0 ]] ; then 14 | usage 15 | exit 1 16 | fi 17 | 18 | py3_check () { 19 | local v=$1 20 | docker build -f docker/$v/Dockerfile docker/$v -t pg-test:$v 21 | docker run -v "${PWD}:/source:ro" pg-test:$v /source/docheck test-in-docker 3.5 22 | } 23 | 24 | 25 | cmd=$1 26 | shift 1 27 | case "${cmd}" in 28 | (py3-11) py3_check py3-11 ;; 29 | (py39-13) py3_check py3-12 ;; 30 | (test-in-docker) 31 | PY_VERSION=$1 32 | cp -r /source /build 33 | cd /build 34 | su postgres -c '/usr/lib/postgresql/*/bin/initdb' 35 | # If jit is enabled, it causes additional output for `explain` 36 | # command, resulting in failed tests. 37 | sed -i -e '/jit =/cjit = false' /var/lib/postgresql/data/postgresql.conf 38 | su postgres -c '/usr/lib/postgresql/*/bin/pg_ctl -D /var/lib/postgresql/data start' 39 | make 40 | make install 41 | make installcheck || /bin/true 42 | if test -f regression.out ; then 43 | cat regression.out 44 | for x in results/*.out ; do 45 | base=$(basename "$x") 46 | echo ">>> $base" 47 | diff -U3 test-${PY_VERSION}/expected/$base $x || /bin/true 48 | done 49 | exit 2 50 | fi 51 | ;; 52 | (*) 53 | usage 54 | exit 1 55 | ;; 56 | esac 57 | -------------------------------------------------------------------------------- /docker/py39-pg13/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:13 2 | RUN apt-get update && apt-get install -y \ 3 | build-essential \ 4 | clang \ 5 | libpython3.9-dev \ 6 | python3.9-dev \ 7 | postgresql-client-13 \ 8 | postgresql-server-dev-13 \ 9 | python3-setuptools 10 | 11 | ENV PGUSER postgres 12 | ENV PYTHON_OVERRIDE python3.9 13 | ENV LC_ALL C.UTF-8 14 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1715534503, 24 | "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "2057814051972fa1453ddfb0d98badbea9b83c06", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "id": "nixpkgs", 32 | "ref": "nixos-unstable", 33 | "type": "indirect" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /multicorn.control: -------------------------------------------------------------------------------- 1 | comment = 'Multicorn2 Python3.9+ bindings for Postgres 13++ Foreign Data Wrapper' 2 | default_version = '3.1' 3 | module_pathname = '$libdir/multicorn' 4 | relocatable = true 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "multicorn" 7 | readme = "README.md" 8 | requires-python = ">=3.9" 9 | license = {text = "Postgresql"} 10 | authors = [{name = "Lussier"}] 11 | dynamic = ["version"] 12 | 13 | [tool.setuptools] 14 | package-dir = {"" = "python"} 15 | packages = ["multicorn", "multicorn.fsfdw"] 16 | include-package-data = false 17 | 18 | [tool.pytest.ini_options] 19 | python_files = ["test*.py"] 20 | -------------------------------------------------------------------------------- /python/multicorn/compat.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | unicode_ = unicode 4 | except NameError: 5 | # Python3 6 | unicode_ = str 7 | 8 | try: 9 | basestring_ = basestring 10 | except NameError: 11 | # Python3 12 | basestring_ = str 13 | 14 | try: 15 | bytes('') 16 | bytes_ = bytes 17 | except TypeError: 18 | # Python3 19 | bytes_ = lambda x: bytes(x, 'utf8') 20 | -------------------------------------------------------------------------------- /python/multicorn/csvfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose 3 | ------- 4 | 5 | This fdw can be used to access data stored in `CSV files`_. Each column defined 6 | in the table will be mapped, in order, against columns in the CSV file. 7 | 8 | .. api_compat:: :read: 9 | 10 | .. _CSV files: http://en.wikipedia.org/wiki/Comma-separated_values 11 | 12 | Dependencies 13 | ------------ 14 | 15 | No dependency outside the standard python distribution. 16 | 17 | Options 18 | ---------------- 19 | 20 | ``filename`` (required) 21 | The full path to the CSV file containing the data. This file must be readable 22 | to the postgres user. 23 | 24 | ``delimiter`` 25 | The CSV delimiter (defaults to ``,``). 26 | 27 | ``quotechar`` 28 | The CSV quote character (defaults to ``"``). 29 | 30 | ``skip_header`` 31 | The number of lines to skip (defaults to ``0``). 32 | 33 | Usage example 34 | ------------- 35 | 36 | Supposing you want to parse the following CSV file, located in ``/tmp/test.csv``:: 37 | 38 | Year,Make,Model,Length 39 | 1997,Ford,E350,2.34 40 | 2000,Mercury,Cougar,2.38 41 | 42 | You can declare the following table: 43 | 44 | .. code-block:: sql 45 | 46 | CREATE SERVER csv_srv foreign data wrapper multicorn options ( 47 | wrapper 'multicorn.csvfdw.CsvFdw' 48 | ); 49 | 50 | 51 | create foreign table csvtest ( 52 | year numeric, 53 | make character varying, 54 | model character varying, 55 | length numeric 56 | ) server csv_srv options ( 57 | filename '/tmp/test.csv', 58 | skip_header '1', 59 | delimiter ','); 60 | 61 | select * from csvtest; 62 | 63 | .. code-block:: bash 64 | 65 | year | make | model | length 66 | ------+---------+--------+-------- 67 | 1997 | Ford | E350 | 2.34 68 | 2000 | Mercury | Cougar | 2.38 69 | (2 lines) 70 | 71 | 72 | """ 73 | 74 | 75 | from . import ForeignDataWrapper 76 | from .utils import log_to_postgres 77 | from logging import WARNING 78 | import csv 79 | 80 | 81 | class CsvFdw(ForeignDataWrapper): 82 | """A foreign data wrapper for accessing csv files. 83 | 84 | Valid options: 85 | - filename : full path to the csv file, which must be readable 86 | by the user running postgresql (usually postgres) 87 | - delimiter : the delimiter used between fields. 88 | Default: "," 89 | """ 90 | 91 | def __init__(self, fdw_options, fdw_columns): 92 | super(CsvFdw, self).__init__(fdw_options, fdw_columns) 93 | self.filename = fdw_options["filename"] 94 | self.delimiter = fdw_options.get("delimiter", ",") 95 | self.quotechar = fdw_options.get("quotechar", '"') 96 | self.skip_header = int(fdw_options.get('skip_header', 0)) 97 | self.columns = fdw_columns 98 | 99 | def execute(self, quals, columns): 100 | with open(self.filename) as stream: 101 | reader = csv.reader(stream, delimiter=self.delimiter) 102 | count = 0 103 | checked = False 104 | for line in reader: 105 | if count >= self.skip_header: 106 | if not checked: 107 | # On first iteration, check if the lines are of the 108 | # appropriate length 109 | checked = True 110 | if len(line) > len(self.columns): 111 | log_to_postgres("There are more columns than " 112 | "defined in the table", WARNING) 113 | if len(line) < len(self.columns): 114 | log_to_postgres("There are less columns than " 115 | "defined in the table", WARNING) 116 | yield line[:len(self.columns)] 117 | count += 1 118 | -------------------------------------------------------------------------------- /python/multicorn/fsfdw/docutils_meta.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use low-level docutils API to extract metadata from ReStructuredText files. 3 | """ 4 | try: 5 | from collections import OrderedDict 6 | except ImportError: 7 | from ordereddict import OrderedDict 8 | from threading import Lock 9 | from functools import wraps 10 | from os.path import getmtime 11 | 12 | from docutils.core import publish_doctree 13 | 14 | 15 | def extract_meta(filename): 16 | """Read meta-data from a reStructuredText file and return a dict. 17 | 18 | The 'title' and 'subtitle' keys are special-cased, but other keys 19 | are read from the `docinfo` element. 20 | 21 | """ 22 | with open(filename) as file_obj: 23 | content = file_obj.read() 24 | meta = {} 25 | for element in publish_doctree(content): 26 | if element.tagname in ('title', 'subtitle'): 27 | meta[element.tagname] = element.astext() 28 | elif element.tagname == 'docinfo': 29 | for field in element: 30 | if field.tagname == 'field': 31 | name, body = field.children 32 | meta[name.astext().lower()] = body.astext() 33 | else: 34 | meta[field.tagname.lower()] = field.astext() 35 | return meta 36 | 37 | 38 | def mtime_lru_cache(function, max_size=100): 39 | """File mtime-based least-recently-used cache. 40 | 41 | :param function: 42 | A function that takes a filename as its single parameter. 43 | The file should exist, and the function's return value should 44 | only depend on the contents of the file. 45 | 46 | Return a decorated function that caches at most the ``max_size`` value. 47 | Least recently used value are dropped first. Cached values are invalidated 48 | when the files's modification time changes. 49 | 50 | Inspired from functools.lru_cache, which only exists in Python 3.2+. 51 | 52 | """ 53 | lock = Lock() # OrderedDict isn't threadsafe 54 | cache = OrderedDict() # ordered least recent to most recent 55 | 56 | @wraps(function) 57 | def wrapper(filename): 58 | mtime = getmtime(filename) 59 | with lock: 60 | if filename in cache: 61 | old_mtime, result = cache.pop(filename) 62 | if old_mtime == mtime: 63 | # Move to the end 64 | cache[filename] = old_mtime, result 65 | return result 66 | result = function(filename) 67 | with lock: 68 | cache[filename] = mtime, result # at the end 69 | if len(cache) > max_size: 70 | cache.popitem(last=False) 71 | return result 72 | return wrapper 73 | -------------------------------------------------------------------------------- /python/multicorn/fsfdw/restfsfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose 3 | ------- 4 | 5 | This fdw can be used to access metadata stored in ReStructured Text files, 6 | in a filesystem. 7 | The files are looked up based on a pattern, and parts of the file's path are 8 | mapped to various columns, as well as the file's content itself. 9 | 10 | The options are exactly the same as ``multicorn.fsfdw`` itself. 11 | 12 | If a column name is prefixed by ``rest_``, it will not be mapped to 13 | a part of the pattern but looked up in the metadata from the ReST document. 14 | """ 15 | from multicorn.fsfdw import FilesystemFdw 16 | 17 | class ReStructuredTextFdw(FilesystemFdw): 18 | """A filesystem with reStructuredText metadata foreign data wrapper. 19 | 20 | The foreign data wrapper accepts the same options as FilesystemFdw. 21 | Any column with a name in rest_* is set to the metadata value with the 22 | corresponding key. (Eg. rest_title is set to the title of the document.) 23 | 24 | """ 25 | def __init__(self, options, columns): 26 | from multicorn.fsfdw.docutils_meta import mtime_lru_cache, extract_meta 27 | # TODO: make max_size configurable? 28 | self.extract_meta = mtime_lru_cache(extract_meta, max_size=1000) 29 | columns = dict((name, column) for name, column in columns.items() 30 | if not name.startswith('rest_')) 31 | super(ReStructuredTextFdw, self).__init__(options, columns) 32 | 33 | def execute(self, quals, columns): 34 | items = self.get_items(quals, columns) 35 | keys = [(name, name[5:]) # len('rest_') == 5 36 | for name in columns if name.startswith('rest_')] 37 | if keys: 38 | items = self.add_meta(items, keys) 39 | return self.items_to_dicts(items, columns) 40 | 41 | def add_meta(self, items, keys): 42 | extract_meta = self.extract_meta 43 | for item in items: 44 | meta = extract_meta(item.full_filename) 45 | for column, key in keys: 46 | item[column] = meta.get(key) 47 | yield item 48 | -------------------------------------------------------------------------------- /python/multicorn/gcfdw.py: -------------------------------------------------------------------------------- 1 | from multicorn import ForeignDataWrapper 2 | import gc 3 | import sys 4 | from multicorn.compat import unicode_ 5 | 6 | 7 | class MyClass(object): 8 | 9 | def __init__(self, num, rand): 10 | self.num = num 11 | self.rand = rand 12 | 13 | 14 | class GCForeignDataWrapper(ForeignDataWrapper): 15 | 16 | def execute(self, quals, columns): 17 | gc.collect() 18 | result = [] 19 | for obj in gc.get_objects(): 20 | tobj = type(obj) 21 | if isinstance(obj, bytes): 22 | obj = obj.decode('utf8') 23 | elif isinstance(obj, unicode_): 24 | pass 25 | else: 26 | try: 27 | obj = bytes(obj).decode('utf8') 28 | except (UnicodeEncodeError, UnicodeDecodeError): 29 | try: 30 | obj = unicode_(obj) 31 | except (UnicodeEncodeError, UnicodeDecodeError): 32 | obj = unicode_("") 33 | result.append({'object': obj, 34 | 'type': unicode_(tobj), 35 | 'id': unicode_(id(obj)), 36 | 'refcount': unicode_(sys.getrefcount(obj))}) 37 | return result 38 | 39 | 40 | class MemStressFDW(ForeignDataWrapper): 41 | 42 | def __init__(self, options, columns): 43 | self.nb = int(options.get('nb', 100000)) 44 | self.options = options 45 | self.columns = columns 46 | super(MemStressFDW, self).__init__(options, columns) 47 | 48 | def execute(self, quals, columns): 49 | for i in range(self.nb): 50 | num = i / 100. 51 | yield {'value': str(MyClass(i, num)), 52 | 'i': i, 53 | 'num': num} 54 | -------------------------------------------------------------------------------- /python/multicorn/gitfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Git foreign data wrapper 3 | 4 | """ 5 | 6 | from . import ForeignDataWrapper 7 | import brigit 8 | 9 | 10 | class GitFdw(ForeignDataWrapper): 11 | """A Git foreign data wrapper. 12 | 13 | The git foreign data wrapper accepts the following options: 14 | 15 | path -- the absolute path to the git repo. It must be readable by 16 | the user running postgresql (usually, postgres). 17 | encoding -- the file encoding. Defaults to "utf-8". 18 | 19 | """ 20 | 21 | def __init__(self, fdw_options, fdw_columns): 22 | super(GitFdw, self).__init__(fdw_options, fdw_columns) 23 | self.path = fdw_options["path"] 24 | self.encoding = fdw_options.get("encoding", "utf-8") 25 | 26 | def execute(self, quals, columns): 27 | def enc(unicode_str): 28 | """Encode the string in the self given encoding.""" 29 | return unicode_str.encode(self.encoding) 30 | for log in brigit.Git(self.path).pretty_log(): 31 | yield { 32 | 'author_name': enc(log["author"]['name']), 33 | 'author_email': enc(log["author"]['email']), 34 | 'message': enc(log['message']), 35 | 'hash': enc(log['hash']), 36 | 'date': log['datetime'].isoformat() 37 | } 38 | -------------------------------------------------------------------------------- /python/multicorn/googlefdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | A foreign data wrapper for performing google searches. 3 | 4 | """ 5 | 6 | from . import ForeignDataWrapper 7 | 8 | import json 9 | import urllib 10 | 11 | 12 | def google(search): 13 | """Retrieves results from google using the json api""" 14 | query = urllib.urlencode({'q': search}) 15 | url = ('http://ajax.googleapis.com/ajax/' 16 | 'services/search/web?v=1.0&%s' % query) 17 | response = urllib.urlopen(url) 18 | results = response.read() 19 | results = json.loads(results) 20 | data = results['responseData'] 21 | hits = data['results'] 22 | for hit in hits: 23 | yield {'url': hit['url'].encode("utf-8"), 24 | 'title': hit["titleNoFormatting"].encode("utf-8"), 25 | 'search': search.encode("utf-8")} 26 | 27 | 28 | class GoogleFdw(ForeignDataWrapper): 29 | """A Google search foreign data wrapper. 30 | 31 | Parses the quals to find anything ressembling a search criteria, and 32 | returns the google search result for it. 33 | Available columns are: url, title, search. 34 | 35 | """ 36 | 37 | def execute(self, quals, columns): 38 | if not quals: 39 | return ("No search specified",) 40 | for qual in quals: 41 | if qual.field_name == "search" or qual.operator == "=": 42 | return google(qual.value) 43 | -------------------------------------------------------------------------------- /python/multicorn/ldapfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose 3 | ------- 4 | 5 | This fdw can be used to access directory servers via the LDAP protocol. 6 | Tested with OpenLDAP. 7 | It supports: simple bind, multiple scopes (subtree, base, etc) 8 | 9 | .. api_compat: :read: 10 | 11 | Dependencies 12 | ------------ 13 | 14 | If using Multicorn >= 1.1.0, you will need the `ldap3`_ library: 15 | 16 | .. _ldap3: http://pythonhosted.org/python3-ldap/ 17 | 18 | For prior version, you will need the `ldap`_ library: 19 | 20 | .. _ldap: http://www.python-ldap.org/ 21 | 22 | Required options 23 | ---------------- 24 | 25 | ``uri`` (string) 26 | The URI for the server, for example "ldap://localhost". 27 | 28 | ``path`` (string) 29 | The base in which the search is performed, for example "dc=example,dc=com". 30 | 31 | ``objectclass`` (string) 32 | The objectClass for which is searched, for example "inetOrgPerson". 33 | 34 | ``scope`` (string) 35 | The scope: one, sub or base. 36 | 37 | Optional options 38 | ---------------- 39 | 40 | ``binddn`` (string) 41 | The binddn for example 'cn=admin,dc=example,dc=com'. 42 | 43 | ``bindpwd`` (string) 44 | The credentials for the binddn. 45 | 46 | Usage Example 47 | ------------- 48 | 49 | To search for a person 50 | definition: 51 | 52 | .. code-block:: sql 53 | 54 | CREATE SERVER ldap_srv foreign data wrapper multicorn options ( 55 | wrapper 'multicorn.ldapfdw.LdapFdw' 56 | ); 57 | 58 | CREATE FOREIGN TABLE ldapexample ( 59 | mail character varying, 60 | cn character varying, 61 | description character varying 62 | ) server ldap_srv options ( 63 | uri 'ldap://localhost', 64 | path 'dc=lab,dc=example,dc=com', 65 | scope 'sub', 66 | binddn 'cn=Admin,dc=example,dc=com', 67 | bindpwd 'admin', 68 | objectClass '*' 69 | ); 70 | 71 | select * from ldapexample; 72 | 73 | .. code-block:: bash 74 | 75 | mail | cn | description 76 | -----------------------+----------------+-------------------- 77 | test@example.com | test | 78 | admin@example.com | admin | LDAP administrator 79 | someuser@example.com | Some Test User | 80 | (3 rows) 81 | 82 | """ 83 | 84 | from . import ForeignDataWrapper 85 | 86 | import ldap3 87 | from multicorn.utils import log_to_postgres, ERROR 88 | from multicorn.compat import unicode_ 89 | 90 | 91 | SPECIAL_CHARS = { 92 | ord('*'): '\\2a', 93 | ord('('): '\\28', 94 | ord(')'): '\29', 95 | ord('\\'): '\\5c', 96 | ord('\x00'): '\\00', 97 | ord('/'): '\\2f' 98 | } 99 | 100 | 101 | class LdapFdw(ForeignDataWrapper): 102 | """An Ldap Foreign Wrapper. 103 | 104 | The following options are required: 105 | 106 | uri -- the ldap URI to connect. (ex: 'ldap://localhost') 107 | address -- the ldap host to connect. (obsolete) 108 | path -- the ldap path (ex: ou=People,dc=example,dc=com) 109 | objectClass -- the ldap object class (ex: 'inetOrgPerson') 110 | scope -- the ldap scope (one, sub or base) 111 | binddn -- the ldap bind DN (ex: 'cn=Admin,dc=example,dc=com') 112 | bindpwd -- the ldap bind Password 113 | 114 | """ 115 | 116 | def __init__(self, fdw_options, fdw_columns): 117 | super(LdapFdw, self).__init__(fdw_options, fdw_columns) 118 | if "address" in fdw_options: 119 | self.ldapuri = "ldap://" + fdw_options["address"] 120 | else: 121 | self.ldapuri = fdw_options["uri"] 122 | self.ldap = ldap3.Connection( 123 | ldap3.Server(self.ldapuri), 124 | user=fdw_options.get("binddn", None), 125 | password=fdw_options.get("bindpwd", None), 126 | client_strategy=ldap3.RESTARTABLE if ldap3.version.__version__ > '2.0.0' else ldap3.STRATEGY_SYNC_RESTARTABLE) 127 | self.path = fdw_options["path"] 128 | self.scope = self.parse_scope(fdw_options.get("scope", None)) 129 | self.object_class = fdw_options["objectclass"] 130 | self.field_list = fdw_columns 131 | self.field_definitions = dict( 132 | (name.lower(), field) for name, field in self.field_list.items()) 133 | self.array_columns = [ 134 | col.column_name for name, col in self.field_definitions.items() 135 | if col.type_name.endswith('[]')] 136 | 137 | def execute(self, quals, columns): 138 | request = unicode_("(objectClass=%s)") % self.object_class 139 | for qual in quals: 140 | if isinstance(qual.operator, tuple): 141 | operator = qual.operator[0] 142 | else: 143 | operator = qual.operator 144 | if operator in ("=", "~~"): 145 | if hasattr(qual.value, "translate"): 146 | baseval = qual.value.translate(SPECIAL_CHARS) 147 | val = (baseval.replace("%", "*") 148 | if operator == "~~" else baseval) 149 | else: 150 | val = qual.value 151 | request = unicode_("(&%s(%s=%s))") % ( 152 | request, qual.field_name, val) 153 | self.ldap.search( 154 | self.path, request, self.scope, 155 | attributes=list(self.field_definitions)) 156 | for entry in self.ldap.response: 157 | # Case insensitive lookup for the attributes 158 | litem = dict() 159 | for key, value in entry["attributes"].items(): 160 | if key.lower() in self.field_definitions: 161 | pgcolname = self.field_definitions[key.lower()].column_name 162 | if ldap3.version.__version__ > '2.0.0': 163 | value = value 164 | else: 165 | if pgcolname in self.array_columns: 166 | value = value 167 | else: 168 | value = value[0] 169 | litem[pgcolname] = value 170 | yield litem 171 | 172 | def parse_scope(self, scope=None): 173 | if scope in (None, "", "one"): 174 | return ldap3.LEVEL if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_SINGLE_LEVEL 175 | elif scope == "sub": 176 | return ldap3.SUBTREE if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_WHOLE_SUBTREE 177 | elif scope == "base": 178 | return ldap3.BASE if ldap3.version.__version__ > '2.0.0' else ldap3.SEARCH_SCOPE_BASE_OBJECT 179 | else: 180 | log_to_postgres("Invalid scope specified: %s" % scope, ERROR) 181 | -------------------------------------------------------------------------------- /python/multicorn/processfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose 3 | ------- 4 | 5 | This foreign data wrapper can be to list processes according to the 6 | psutil module. 7 | 8 | The column names are mapped to the :py:class:`psutil.Process` attributes. 9 | 10 | .. api_compat: :read: 11 | 12 | Usage Example 13 | ------------- 14 | 15 | .. code-block:: sql 16 | 17 | create foreign table processes ( 18 | pid int, 19 | ppid int, 20 | name text, 21 | exe text, 22 | cmdline text, 23 | create_time timestamptz, 24 | status text, 25 | cwd text, 26 | uids int[], 27 | gids int[], 28 | terminal text, 29 | nice int, 30 | ionice float[], 31 | rlimit int[], 32 | num_ctx_switches bigint[], 33 | num_fds int, 34 | num_threads int, 35 | cpu_times interval[], 36 | cpu_percent float, 37 | cpu_affinity bigint[], 38 | memory_info bigint[], 39 | memory_percent float, 40 | open_files text[], 41 | connections text[], 42 | is_running bool 43 | ) server process_server 44 | 45 | 46 | .. code-block:: bash 47 | 48 | ro= select name, cmdline, cpu_percent from processes where name = 'postgres'; 49 | name | cmdline | cpu_percent 50 | ----------+----------------------------------------------+------------- 51 | postgres | ['/home/ro/pgdev/bin/postgres'] | 0 52 | postgres | ['postgres: checkpointer process '] | 0 53 | postgres | ['postgres: writer process '] | 0 54 | postgres | ['postgres: wal writer process '] | 0 55 | postgres | ['postgres: autovacuum launcher process '] | 0 56 | postgres | ['postgres: stats collector process '] | 0 57 | postgres | ['postgres: ro ro [local] SELECT'] | 9 58 | (7 rows) 59 | 60 | 61 | 62 | Options 63 | ------- 64 | 65 | No options. 66 | 67 | """ 68 | from . import ForeignDataWrapper 69 | from datetime import datetime 70 | import psutil 71 | 72 | 73 | DATE_COLUMNS = ['create_time'] 74 | 75 | 76 | class ProcessFdw(ForeignDataWrapper): 77 | """A foreign datawrapper for querying system stats. 78 | 79 | It accepts no options. 80 | You can define any column named after a statgrab column. 81 | See the statgrab documentation. 82 | 83 | """ 84 | 85 | def _convert(self, key, value): 86 | if key in DATE_COLUMNS: 87 | if isinstance(value, (list, tuple)): 88 | return [datetime.fromtimestamp(v) for v in value] 89 | else: 90 | return datetime.fromtimestamp(value) 91 | return value 92 | 93 | def execute(self, quals, columns): 94 | for process in psutil.process_iter(): 95 | yield dict([(key, self._convert(key, value)) 96 | for key, value in process.as_dict(columns).items()]) 97 | -------------------------------------------------------------------------------- /python/multicorn/rssfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Purpose 3 | ------- 4 | 5 | This fdw can be used to access items from an rss feed. 6 | The column names are mapped to the elements inside an item. 7 | An rss item has the following strcture: 8 | 9 | .. code-block:: xml 10 | 11 | 12 | Title 13 | 2011-01-02 14 | http://example.com/test 15 | http://example.com/test 16 | Small description 17 | 18 | 19 | You can access every element by defining a column with the same name. Be 20 | careful to match the case! Example: pubDate should be quoted like this: 21 | ``pubDate`` to preserve the uppercased ``D``. 22 | 23 | 24 | .. api_compat:: 25 | :read: 26 | 27 | Dependencies 28 | ------------ 29 | 30 | You will need the `lxml`_ library. 31 | 32 | .. _lxml: http://lxml.de/ 33 | 34 | Required options 35 | ----------------- 36 | 37 | ``url`` (string) 38 | The RSS feed URL. 39 | 40 | Usage Example 41 | ------------- 42 | 43 | .. _Radicale: http://radicale.org 44 | 45 | If you want to parse the `radicale`_ rss feed, you can use the following 46 | definition: 47 | 48 | .. code-block:: sql 49 | 50 | CREATE SERVER rss_srv foreign data wrapper multicorn options ( 51 | wrapper 'multicorn.rssfdw.RssFdw' 52 | ); 53 | 54 | CREATE FOREIGN TABLE radicalerss ( 55 | "pubDate" timestamp, 56 | description character varying, 57 | title character varying, 58 | link character varying 59 | ) server rss_srv options ( 60 | url 'http://radicale.org/rss/' 61 | ); 62 | 63 | select "pubDate", title, link from radicalerss limit 10; 64 | 65 | .. code-block:: bash 66 | 67 | pubDate | title | link 68 | ---------------------+----------------------------------+---------------------------------------------- 69 | 2011-09-27 06:07:42 | Radicale 0.6.2 | http://radicale.org/news#2011-09-27@06:07:42 70 | 2011-08-28 13:20:46 | Radicale 0.6.1, Changes, Future | http://radicale.org/news#2011-08-28@13:20:46 71 | 2011-08-01 08:54:43 | Radicale 0.6 Released | http://radicale.org/news#2011-08-01@08:54:43 72 | 2011-07-02 20:13:29 | Feature Freeze for 0.6 | http://radicale.org/news#2011-07-02@20:13:29 73 | 2011-05-01 17:24:33 | Ready for WSGI | http://radicale.org/news#2011-05-01@17:24:33 74 | 2011-04-30 10:21:12 | Apple iCal Support | http://radicale.org/news#2011-04-30@10:21:12 75 | 2011-04-25 22:10:59 | Two Features and One New Roadmap | http://radicale.org/news#2011-04-25@22:10:59 76 | 2011-04-10 20:04:33 | New Features | http://radicale.org/news#2011-04-10@20:04:33 77 | 2011-04-02 12:11:37 | Radicale 0.5 Released | http://radicale.org/news#2011-04-02@12:11:37 78 | 2011-02-03 23:35:55 | Jabber Room and iPhone Support | http://radicale.org/news#2011-02-03@23:35:55 79 | (10 lignes) 80 | """ 81 | 82 | from . import ForeignDataWrapper 83 | from datetime import datetime, timedelta 84 | from lxml import etree 85 | try: 86 | from urllib.request import urlopen 87 | except ImportError: 88 | from urllib import urlopen 89 | from logging import ERROR, WARNING 90 | from multicorn.utils import log_to_postgres 91 | import json 92 | 93 | 94 | def element_to_dict(element): 95 | """ 96 | This method takes a lxml element and return a json string containing 97 | the element attributes and a text key and a child node. 98 | >>> test = lambda x: sorted([(k, sorted(v.items())) if isinstance(v, dict) else (k, [sorted(e.items()) for e in v]) if isinstance(v, list) else (k, v) for k, v in element_to_dict(etree.fromstring(x)).items()]) 99 | >>> test('') 100 | [('attributes', {'a1': 'v1'}), ('children', []), ('tag', 't'), ('text', '')] 101 | 102 | >>> test('Txt') 103 | [('attributes', {'a1': 'v1'}), ('children', []), ('tag', 't'), ('text', 'Txt')] 104 | 105 | >>> test('TxtSub1Txt2Sub2Txt3') 106 | [('attributes', {}), ('children', [[('attributes', {'a1': 'v1'}), ('children', []), ('tag', 's1'), ('text', 'Sub1')], [('attributes', {'a2': 'v2'}), ('children', []), ('tag', 's2'), ('text', 'Sub2')]]), ('tag', 't'), ('text', 'Txt')] 107 | 108 | """ 109 | return { 110 | 'tag': etree.QName(element.tag).localname, 111 | 'text': element.text or '', 112 | 'attributes': dict(element.attrib), 113 | 'children': [element_to_dict(e) for e in element] 114 | } 115 | 116 | 117 | class RssFdw(ForeignDataWrapper): 118 | """An rss foreign data wrapper. 119 | 120 | The following options are accepted: 121 | 122 | url -- The rss feed urls. 123 | 124 | The columns named are parsed, and are used as xpath expression on 125 | each item xml node. Exemple: a column named "pubDate" would return the 126 | pubDate element of an rss item. 127 | 128 | """ 129 | 130 | def __init__(self, options, columns): 131 | super(RssFdw, self).__init__(options, columns) 132 | self.url = options.get('url', None) 133 | self.cache = (None, None) 134 | self.cache_duration = options.get('cache_duration', None) 135 | if self.cache_duration is not None: 136 | self.cache_duration = timedelta(seconds=int(self.cache_duration)) 137 | if self.url is None: 138 | log_to_postgres("You MUST set an url when creating the table!", 139 | ERROR) 140 | self.columns = columns 141 | self.default_namespace_prefix = options.pop( 142 | 'default_namespace_prefix', None) 143 | self.item_root = options.pop('item_root', 'item') 144 | 145 | def get_namespaces(self, xml): 146 | ns = dict(xml.nsmap) 147 | if None in ns: 148 | ns[self.default_namespace_prefix] = ns.pop(None) 149 | return ns 150 | 151 | def make_item_from_xml(self, xml_elem): 152 | """Internal method used for parsing item xml element from the 153 | columns definition.""" 154 | item = {} 155 | for prop, column in self.columns.items(): 156 | value = xml_elem.xpath( 157 | prop, namespaces=self.get_namespaces(xml_elem)) 158 | if value: 159 | if column.type_name.startswith('json'): 160 | item[prop] = json.dumps([ 161 | element_to_dict(val) for val in value]) 162 | # There should be a better way 163 | # oid is 1009 ? 164 | elif column.type_name.endswith('[]'): 165 | item[prop] = [elem.text for elem in value] 166 | else: 167 | item[prop] = getattr(value[0], 'text', value[0]) 168 | return item 169 | 170 | def execute(self, quals, columns): 171 | """Quals are ignored.""" 172 | if self.cache_duration is not None: 173 | date, values = self.cache 174 | if values is not None: 175 | if (datetime.now() - date) < self.cache_duration: 176 | return values 177 | try: 178 | xml = etree.fromstring(urlopen(self.url).read()) 179 | items = [self.make_item_from_xml(elem) 180 | for elem in xml.xpath( 181 | '//%s' % self.item_root, 182 | namespaces=self.get_namespaces(xml))] 183 | self.cache = (datetime.now(), items) 184 | return items 185 | except etree.ParseError: 186 | log_to_postgres("Malformed xml, returning nothing") 187 | except IOError: 188 | log_to_postgres("Cannot retrieve '%s'" % self.url, WARNING) 189 | -------------------------------------------------------------------------------- /python/multicorn/statefdw.py: -------------------------------------------------------------------------------- 1 | """A dummy foreign data wrapper""" 2 | 3 | 4 | from . import ForeignDataWrapper 5 | from .utils import log_to_postgres 6 | from logging import ERROR, DEBUG, INFO, WARNING 7 | 8 | 9 | class StateFdw(ForeignDataWrapper): 10 | """A dummy foreign data wrapper. 11 | 12 | This dummy foreign data wrapper is intended as a proof of concept of state 13 | keeping foreign data wrappers. 14 | 15 | It keeps an internal state as an integer, auto-incremented at each request. 16 | """ 17 | 18 | def __init__(self, *args): 19 | super(StateFdw, self).__init__(*args) 20 | self.state = 0 21 | 22 | def execute(self, quals, columns): 23 | self.state += 1 24 | yield [self.state] 25 | -------------------------------------------------------------------------------- /python/multicorn/utils.py: -------------------------------------------------------------------------------- 1 | from logging import ERROR, INFO, DEBUG, WARNING, CRITICAL 2 | try: 3 | from ._utils import _log_to_postgres 4 | from ._utils import check_interrupts 5 | except ImportError as e: 6 | from warnings import warn 7 | warn("Not executed in a postgresql server," 8 | " disabling log_to_postgres", ImportWarning) 9 | 10 | def _log_to_postgres(message, level=0, hint=None, detail=None): 11 | pass 12 | 13 | 14 | REPORT_CODES = { 15 | DEBUG: 0, 16 | INFO: 1, 17 | WARNING: 2, 18 | ERROR: 3, 19 | CRITICAL: 4 20 | } 21 | 22 | 23 | class MulticornException(Exception): 24 | def __init__(self, message, code, hint, detail): 25 | self._is_multicorn_exception = True 26 | self.message = message 27 | self.code = code 28 | self.hint = hint 29 | self.detail = detail 30 | 31 | 32 | def log_to_postgres(message, level=INFO, hint=None, detail=None): 33 | code = REPORT_CODES.get(level, None) 34 | if code is None: 35 | raise KeyError("Not a valid log level") 36 | if level in (ERROR, CRITICAL): 37 | # if we sent an ERROR or FATAL(=CRITICAL) message to _log_to_postgres, we would trigger the PostgreSQL C-level 38 | # exception handling, which would prevent us from cleanly exiting whatever Python context we're currently in. 39 | # To avoid this, these log levels are replaced with exceptions which are bubbled back to Multicorn's entry 40 | # points, and those exceptions are translated into appropriate logging after we exit the method at the top of 41 | # the multicorn stack. 42 | raise MulticornException(message, code, hint, detail) 43 | else: 44 | _log_to_postgres(message, code, hint=hint, detail=detail) 45 | -------------------------------------------------------------------------------- /python/multicorn/xmlfdw.py: -------------------------------------------------------------------------------- 1 | """ 2 | An XML Foreign Data Wrapper. 3 | """ 4 | 5 | from . import ForeignDataWrapper 6 | from xml.sax import ContentHandler, make_parser 7 | 8 | 9 | class MulticornXMLHandler(ContentHandler): 10 | 11 | def __init__(self, elem_tag, columns): 12 | self.elem_tag = elem_tag 13 | self.columns = columns 14 | self.reset() 15 | 16 | def reset(self): 17 | self.parsed_rows = [] 18 | self.current_row = {} 19 | self.tag = None 20 | self.root_seen = 0 21 | self.nested = False 22 | 23 | def startElement(self, name, attrs): 24 | if name == self.elem_tag: 25 | # Keep track of nested "elem_tag" 26 | self.root_seen += 1 27 | elif self.root_seen == 1: 28 | # Ignore nested tag. 29 | if name in self.columns: 30 | self.tag = name 31 | self.current_row[name] = '' 32 | 33 | def characters(self, content): 34 | if self.tag is not None: 35 | self.current_row[self.tag] += content 36 | 37 | def get_rows(self): 38 | """Return the parsed_rows, and forget about it.""" 39 | result, self.parsed_rows = self.parsed_rows, [] 40 | return result 41 | 42 | def endElement(self, name): 43 | if name == self.elem_tag: 44 | self.root_seen -= 1 45 | self.parsed_rows.append(self.current_row) 46 | self.current_row = {} 47 | elif name in self.columns: 48 | self.tag = None 49 | 50 | 51 | class XMLFdw(ForeignDataWrapper): 52 | """A foreign data wrapper for accessing xml files. 53 | 54 | Valid options: 55 | - filename: full path to the xml file. 56 | - elem_tag: a tagname acting as a root for a tag. 57 | Child tag will be mapped to corresponding columns. 58 | """ 59 | 60 | def __init__(self, fdw_options, fdw_columns): 61 | super(XMLFdw, self).__init__(fdw_options, fdw_columns) 62 | self.filename = fdw_options['filename'] 63 | self.elem_tag = fdw_options['elem_tag'] 64 | self.buffer_size = fdw_options.get('buffer_size', 4096) 65 | self.columns = fdw_columns 66 | 67 | def execute(self, quals, columns): 68 | parser = make_parser() 69 | handler = MulticornXMLHandler(self.elem_tag, self.columns) 70 | parser.setContentHandler(handler) 71 | with open(self.filename) as stream: 72 | while(True): 73 | a = stream.read(self.buffer_size) 74 | if not a: 75 | break 76 | parser.feed(a) 77 | for row in handler.get_rows(): 78 | yield row 79 | parser.close() 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from setuptools import setup, find_packages, Extension 4 | 5 | 6 | # hum... borrowed from psycopg2 7 | def get_pg_config(kind, pg_config="pg_config"): 8 | p = subprocess.Popen([pg_config, '--%s' % kind], stdout=subprocess.PIPE) 9 | r = p.communicate() 10 | r = r[0].strip().decode('utf8') 11 | if not r: 12 | raise Warning(p[2].readline()) 13 | return r 14 | 15 | 16 | include_dirs = [get_pg_config(d) for d in ("includedir", "pkgincludedir", "includedir-server")] 17 | 18 | multicorn_utils_module = Extension('multicorn._utils', 19 | include_dirs=include_dirs, 20 | extra_compile_args=['-shared'], 21 | sources=['src/utils.c']) 22 | 23 | 24 | def get_version(): 25 | script_dir = os.path.dirname(os.path.realpath(__file__)) 26 | with open(os.path.join(script_dir, 'multicorn.control')) as f: 27 | for line in f: 28 | if line.startswith('default_version'): 29 | return line.strip().split("'")[-2] 30 | raise RuntimeError('default_version not found in multicorn.control') 31 | 32 | 33 | setup( 34 | version=get_version(), 35 | ext_modules=[multicorn_utils_module] 36 | ) 37 | -------------------------------------------------------------------------------- /sql/multicorn--1.4.0--2.2.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/sql/multicorn--1.4.0--2.2.sql -------------------------------------------------------------------------------- /sql/multicorn--1.4.0--2.3.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/sql/multicorn--1.4.0--2.3.sql -------------------------------------------------------------------------------- /sql/multicorn--2.2--2.3.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/sql/multicorn--2.2--2.3.sql -------------------------------------------------------------------------------- /sql/multicorn--2.2--2.4.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/sql/multicorn--2.2--2.4.sql -------------------------------------------------------------------------------- /sql/multicorn--2.2.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /sql/multicorn--2.3--2.4.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsql-io/multicorn2/db1a34e2257e513cc5e561f303fad93746ed4c04/sql/multicorn--2.3--2.4.sql -------------------------------------------------------------------------------- /sql/multicorn--2.3.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /sql/multicorn--2.4.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /sql/multicorn--2.5.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /sql/multicorn--3.0.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /sql/multicorn.sql: -------------------------------------------------------------------------------- 1 | -- create wrapper with validator and handler 2 | CREATE OR REPLACE FUNCTION multicorn_validator (text[], oid) 3 | RETURNS bool 4 | AS 'MODULE_PATHNAME' 5 | LANGUAGE C STRICT; 6 | 7 | CREATE OR REPLACE FUNCTION multicorn_handler () 8 | RETURNS fdw_handler 9 | AS 'MODULE_PATHNAME' 10 | LANGUAGE C STRICT; 11 | 12 | CREATE FOREIGN DATA WRAPPER multicorn 13 | VALIDATOR multicorn_validator HANDLER multicorn_handler; 14 | -------------------------------------------------------------------------------- /src/errors.c: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------- 2 | * 3 | * The Multicorn Foreign Data Wrapper allows you to fetch foreign data in 4 | * Python in your PostgreSQL server. 5 | * 6 | * This module contains error handling functions. 7 | * 8 | * This software is released under the postgresql licence 9 | * 10 | *------------------------------------------------------------------------- 11 | */ 12 | #include "multicorn.h" 13 | #include "bytesobject.h" 14 | #include "access/xact.h" 15 | 16 | void reportException(PyObject *pErrType, 17 | PyObject *pErrValue, 18 | PyObject *pErrTraceback); 19 | 20 | void reportMulticornException(PyObject *pErrValue); 21 | 22 | PGDLLEXPORT void 23 | errorCheck() 24 | { 25 | PyObject *pErrType, 26 | *pErrValue, 27 | *pErrTraceback; 28 | 29 | PyErr_Fetch(&pErrType, &pErrValue, &pErrTraceback); 30 | if (pErrType) 31 | { 32 | // if the error value has a property _is_multicorn_exception and a boolean value True, then we don't report the 33 | // error as a generic exception with a stack trace -- instead we just take the message, code(severity), hint, 34 | // and detail, and log it to Postgres. These exceptions are generated in utils.py to intercept ERROR/FATAL log 35 | // messages. So, first detect whether that's the case, and call a new reporting function... 36 | PyObject *is_multicorn_exception = PyObject_GetAttrString(pErrValue, "_is_multicorn_exception"); 37 | // clear AttributeError possibly raised by PyObject_GetAttrString 38 | PyErr_Clear(); 39 | if (is_multicorn_exception != NULL && PyObject_IsTrue(is_multicorn_exception)) 40 | { 41 | Py_DECREF(is_multicorn_exception); 42 | Py_DECREF(pErrType); 43 | Py_DECREF(pErrTraceback); 44 | reportMulticornException(pErrValue); 45 | } 46 | else 47 | { 48 | reportException(pErrType, pErrValue, pErrTraceback); 49 | } 50 | } 51 | } 52 | 53 | void 54 | reportException(PyObject *pErrType, PyObject *pErrValue, PyObject *pErrTraceback) 55 | { 56 | char *errName, 57 | *errValue, 58 | *errTraceback = ""; 59 | PyObject *traceback_list; 60 | PyObject *pTemp; 61 | PyObject *tracebackModule = PyImport_ImportModule("traceback"); 62 | PyObject *format_exception = PyObject_GetAttrString(tracebackModule, "format_exception"); 63 | PyObject *newline = PyString_FromString("\n"); 64 | int severity; 65 | 66 | PyErr_NormalizeException(&pErrType, &pErrValue, &pErrTraceback); 67 | pTemp = PyObject_GetAttrString(pErrType, "__name__"); 68 | errName = PyString_AsString(pTemp); 69 | errValue = PyString_AsString(PyObject_Str(pErrValue)); 70 | if (pErrTraceback != NULL) 71 | { 72 | traceback_list = PyObject_CallFunction(format_exception, "(O,O,O)", pErrType, pErrValue, pErrTraceback); 73 | errTraceback = PyString_AsString(PyObject_CallMethod(newline, "join", "(O)", traceback_list)); 74 | Py_DECREF(pErrTraceback); 75 | Py_DECREF(traceback_list); 76 | } 77 | 78 | if (IsAbortedTransactionBlockState()) 79 | { 80 | severity = WARNING; 81 | } 82 | else 83 | { 84 | severity = ERROR; 85 | } 86 | 87 | if (errstart(severity, TEXTDOMAIN)) 88 | { 89 | 90 | if (errstart(severity, TEXTDOMAIN)) 91 | errmsg("Error in python: %s", errName); 92 | errdetail("%s", errValue); 93 | errdetail_log("%s", errTraceback); 94 | } 95 | Py_DECREF(pErrType); 96 | Py_DECREF(pErrValue); 97 | Py_DECREF(format_exception); 98 | Py_DECREF(tracebackModule); 99 | Py_DECREF(newline); 100 | Py_DECREF(pTemp); 101 | errfinish(__FILE__, __LINE__, PG_FUNCNAME_MACRO); 102 | } 103 | 104 | void reportMulticornException(PyObject* pErrValue) 105 | { 106 | int severity; 107 | PyObject *message = PyObject_GetAttrString(pErrValue, "message"); 108 | PyObject *hint = PyObject_GetAttrString(pErrValue, "hint"); 109 | PyObject *detail = PyObject_GetAttrString(pErrValue, "detail"); 110 | PyObject *code = PyObject_GetAttrString(pErrValue, "code"); 111 | int level = PyLong_AsLong(code); 112 | 113 | // Matches up with REPORT_CODES in utils.py 114 | switch (level) 115 | { 116 | case 3: 117 | severity = ERROR; 118 | break; 119 | default: 120 | case 4: 121 | severity = FATAL; 122 | break; 123 | } 124 | 125 | PG_TRY(); 126 | { 127 | 128 | if (errstart(severity, TEXTDOMAIN)) 129 | { 130 | errmsg("%s", PyString_AsString(message)); 131 | if (hint != NULL && hint != Py_None) 132 | { 133 | char* hintstr = PyString_AsString(hint); 134 | errhint("%s", hintstr); 135 | } 136 | if (detail != NULL && detail != Py_None) 137 | { 138 | char* detailstr = PyString_AsString(detail); 139 | errdetail("%s", detailstr); 140 | } 141 | errfinish(__FILE__, __LINE__, PG_FUNCNAME_MACRO); 142 | } 143 | 144 | } 145 | PG_CATCH(); 146 | { 147 | Py_DECREF(message); 148 | Py_DECREF(hint); 149 | Py_DECREF(detail); 150 | Py_DECREF(code); 151 | Py_DECREF(pErrValue); 152 | PG_RE_THROW(); 153 | } 154 | PG_END_TRY(); 155 | } 156 | -------------------------------------------------------------------------------- /src/multicorn.h: -------------------------------------------------------------------------------- 1 | #include "Python.h" 2 | #include "postgres.h" 3 | #include "access/relscan.h" 4 | #include "catalog/pg_foreign_server.h" 5 | #include "catalog/pg_foreign_table.h" 6 | #include "catalog/pg_type.h" 7 | #include "commands/defrem.h" 8 | #include "commands/explain.h" 9 | #include "foreign/fdwapi.h" 10 | #include "foreign/foreign.h" 11 | #include "funcapi.h" 12 | #include "lib/stringinfo.h" 13 | #include "nodes/bitmapset.h" 14 | #include "nodes/makefuncs.h" 15 | #include "nodes/pg_list.h" 16 | 17 | #ifndef PG_FUNCNAME_MACRO 18 | #define PG_FUNCNAME_MACRO __func__ 19 | #endif 20 | 21 | #include "utils/builtins.h" 22 | #include "utils/syscache.h" 23 | 24 | #ifndef PG_MULTICORN_H 25 | #define PG_MULTICORN_H 26 | 27 | /* Data structures */ 28 | 29 | #define C_LOG(...) do { \ 30 | errstart(NOTICE, __FILE__, __LINE__, PG_FUNCNAME_MACRO, TEXTDOMAIN); \ 31 | errmsg(__VA_ARGS__); \ 32 | errfinish(0); \ 33 | } while (0) 34 | 35 | 36 | typedef struct CacheEntry 37 | { 38 | Oid hashkey; 39 | PyObject *value; 40 | List *options; 41 | List *columns; 42 | int xact_depth; 43 | /* Keep the "options" and "columns" in a specific context to avoid leaks. */ 44 | MemoryContext cacheContext; 45 | } CacheEntry; 46 | 47 | 48 | typedef struct ConversionInfo 49 | { 50 | char *attrname; 51 | FmgrInfo *attinfunc; 52 | FmgrInfo *attoutfunc; 53 | Oid atttypoid; 54 | Oid attioparam; 55 | int32 atttypmod; 56 | int attnum; 57 | bool is_array; 58 | int attndims; 59 | bool need_quote; 60 | } ConversionInfo; 61 | 62 | 63 | typedef struct MulticornPlanState 64 | { 65 | Oid foreigntableid; 66 | AttrNumber numattrs; 67 | PyObject *fdw_instance; 68 | List *target_list; 69 | List *qual_list; 70 | int startupCost; 71 | ConversionInfo **cinfos; 72 | List *pathkeys; /* list of MulticornDeparsedSortGroup) */ 73 | 74 | /* For some reason, `baserel->reltarget->width` gets changed 75 | * outside of our control somewhere between GetForeignPaths and 76 | * GetForeignPlan, which breaks tests. 77 | * 78 | * XXX: This is very crude hack to transfer width, calculated by 79 | * getRelSize to GetForeignPlan. 80 | */ 81 | int width; 82 | } MulticornPlanState; 83 | 84 | typedef struct MulticornExecState 85 | { 86 | /* instance and iterator */ 87 | PyObject *fdw_instance; 88 | PyObject *p_iterator; 89 | /* Information carried from the plan phase. */ 90 | List *target_list; 91 | List *qual_list; 92 | Datum *values; 93 | bool *nulls; 94 | ConversionInfo **cinfos; 95 | /* Common buffer to avoid repeated allocations */ 96 | StringInfo buffer; 97 | AttrNumber rowidAttno; 98 | char *rowidAttrName; 99 | List *pathkeys; /* list of MulticornDeparsedSortGroup) */ 100 | } MulticornExecState; 101 | 102 | typedef struct MulticornModifyState 103 | { 104 | ConversionInfo **cinfos; 105 | ConversionInfo **resultCinfos; 106 | PyObject *fdw_instance; 107 | StringInfo buffer; 108 | AttrNumber rowidAttno; 109 | char *rowidAttrName; 110 | ConversionInfo *rowidCinfo; 111 | } MulticornModifyState; 112 | 113 | 114 | typedef struct MulticornBaseQual 115 | { 116 | AttrNumber varattno; 117 | NodeTag right_type; 118 | Oid typeoid; 119 | char *opname; 120 | bool isArray; 121 | bool useOr; 122 | } MulticornBaseQual; 123 | 124 | typedef struct MulticornConstQual 125 | { 126 | MulticornBaseQual base; 127 | Datum value; 128 | bool isnull; 129 | } MulticornConstQual; 130 | 131 | typedef struct MulticornVarQual 132 | { 133 | MulticornBaseQual base; 134 | AttrNumber rightvarattno; 135 | } MulticornVarQual; 136 | 137 | typedef struct MulticornParamQual 138 | { 139 | MulticornBaseQual base; 140 | Expr *expr; 141 | } MulticornParamQual; 142 | 143 | typedef struct MulticornDeparsedSortGroup 144 | { 145 | Name attname; 146 | int attnum; 147 | bool reversed; 148 | bool nulls_first; 149 | Name collate; 150 | PathKey *key; 151 | } MulticornDeparsedSortGroup; 152 | 153 | /* errors.c */ 154 | void errorCheck(void); 155 | 156 | /* python.c */ 157 | PyObject *pgstringToPyUnicode(const char *string); 158 | char **pyUnicodeToPgString(PyObject *pyobject); 159 | 160 | PyObject *getInstance(Oid foreigntableid); 161 | PyObject *qualToPyObject(Expr *expr, PlannerInfo *root); 162 | PyObject *getClassString(const char *className); 163 | PyObject *execute(ForeignScanState *state, ExplainState *es); 164 | void pythonResultToTuple(PyObject *p_value, 165 | TupleTableSlot *slot, 166 | ConversionInfo ** cinfos, 167 | StringInfo buffer); 168 | PyObject *tupleTableSlotToPyObject(TupleTableSlot *slot, ConversionInfo ** cinfos); 169 | char *getRowIdColumn(PyObject *fdw_instance); 170 | int getModifyBatchSize(PyObject *fdw_instance); 171 | PyObject *optionsListToPyDict(List *options); 172 | const char *getPythonEncodingName(void); 173 | 174 | void getRelSize(MulticornPlanState * state, 175 | PlannerInfo *root, 176 | double *rows, 177 | int *width); 178 | 179 | List *pathKeys(MulticornPlanState * state); 180 | 181 | List *canSort(MulticornPlanState * state, List *deparsed); 182 | 183 | CacheEntry *getCacheEntry(Oid foreigntableid); 184 | UserMapping *multicorn_GetUserMapping(Oid userid, Oid serverid); 185 | 186 | 187 | /* Hash table mapping oid to fdw instances */ 188 | extern PGDLLIMPORT HTAB *InstancesHash; 189 | 190 | 191 | /* query.c */ 192 | void extractRestrictions( 193 | #if PG_VERSION_NUM >= 140000 194 | PlannerInfo *root, 195 | #endif 196 | Relids base_relids, 197 | Expr *node, 198 | List **quals); 199 | 200 | List *extractColumns(List *reltargetlist, List *restrictinfolist); 201 | void initConversioninfo(ConversionInfo ** cinfo, 202 | AttInMetadata *attinmeta); 203 | 204 | #if PG_VERSION_NUM < 150000 205 | Value 206 | #else 207 | String 208 | #endif 209 | *colnameFromVar(Var *var, PlannerInfo *root, 210 | MulticornPlanState * state); 211 | 212 | void computeDeparsedSortGroup(List *deparsed, MulticornPlanState *planstate, 213 | List **apply_pathkeys, 214 | List **deparsed_pathkeys); 215 | 216 | List *findPaths(PlannerInfo *root, RelOptInfo *baserel, List *possiblePaths, 217 | int startupCost, 218 | MulticornPlanState *state, 219 | List *apply_pathkeys, List *deparsed_pathkeys); 220 | 221 | List *deparse_sortgroup(PlannerInfo *root, Oid foreigntableid, RelOptInfo *rel); 222 | 223 | PyObject *datumToPython(Datum node, Oid typeoid, ConversionInfo * cinfo); 224 | 225 | List *serializeDeparsedSortGroup(List *pathkeys); 226 | List *deserializeDeparsedSortGroup(List *items); 227 | 228 | #endif /* PG_MULTICORN_H */ 229 | 230 | char *PyUnicode_AsPgString(PyObject *p_unicode); 231 | 232 | PyObject *PyString_FromString(const char *s); 233 | PyObject *PyString_FromStringAndSize(const char *s, Py_ssize_t size); 234 | char *PyString_AsString(PyObject *unicode); 235 | int PyString_AsStringAndSize(PyObject *unicode, char **tempbuffer, Py_ssize_t *length); 236 | 237 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------- 2 | * 3 | * The Multicorn Foreign Data Wrapper allows you to fetch foreign data in 4 | * Python in your PostgreSQL. 5 | * 6 | * This module contains helpers meant to be called from python code. 7 | * 8 | * This software is released under the postgresql licence 9 | * 10 | *------------------------------------------------------------------------- 11 | */ 12 | #include 13 | #include "postgres.h" 14 | #include "multicorn.h" 15 | #include "miscadmin.h" 16 | 17 | 18 | struct module_state 19 | { 20 | PyObject *error; 21 | }; 22 | 23 | #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) 24 | 25 | static PyObject * 26 | log_to_postgres(PyObject *self, PyObject *args, PyObject *kwargs) 27 | { 28 | char *message = NULL; 29 | char *hintstr = NULL, 30 | *detailstr = NULL; 31 | int level = 1; 32 | int severity; 33 | PyObject *hint, 34 | *p_message, 35 | *detail; 36 | 37 | if (!PyArg_ParseTuple(args, "O|i", &p_message, &level)) 38 | { 39 | errorCheck(); 40 | Py_INCREF(Py_None); 41 | return Py_None; 42 | } 43 | if (PyBytes_Check(p_message)) 44 | { 45 | message = PyBytes_AsString(p_message); 46 | } 47 | else if (PyUnicode_Check(p_message)) 48 | { 49 | message = strdup(PyUnicode_AsPgString(p_message)); 50 | } 51 | else 52 | { 53 | 54 | PyObject *temp = PyObject_Str(p_message); 55 | 56 | errorCheck(); 57 | message = strdup(PyString_AsString(temp)); 58 | errorCheck(); 59 | Py_DECREF(temp); 60 | } 61 | switch (level) 62 | { 63 | case 0: 64 | severity = DEBUG1; 65 | break; 66 | case 1: 67 | severity = NOTICE; 68 | break; 69 | case 2: 70 | severity = WARNING; 71 | break; 72 | case 3: 73 | severity = ERROR; 74 | break; 75 | case 4: 76 | severity = FATAL; 77 | break; 78 | default: 79 | severity = INFO; 80 | break; 81 | } 82 | hint = PyDict_GetItemString(kwargs, "hint"); 83 | detail = PyDict_GetItemString(kwargs, "detail"); 84 | 85 | if (errstart(severity, TEXTDOMAIN)) 86 | { 87 | errmsg("%s", message); 88 | if (hint != NULL && hint != Py_None) 89 | { 90 | hintstr = PyString_AsString(hint); 91 | errhint("%s", hintstr); 92 | } 93 | if (detail != NULL && detail != Py_None) 94 | { 95 | detailstr = PyString_AsString(detail); 96 | errdetail("%s", detailstr); 97 | } 98 | Py_DECREF(args); 99 | Py_DECREF(kwargs); 100 | errfinish(__FILE__, __LINE__, PG_FUNCNAME_MACRO); 101 | } 102 | else 103 | { 104 | Py_DECREF(args); 105 | Py_DECREF(kwargs); 106 | } 107 | Py_INCREF(Py_None); 108 | return Py_None; 109 | } 110 | 111 | static PyObject * 112 | py_check_interrupts(PyObject *self, PyObject *args, PyObject *kwargs) 113 | { 114 | CHECK_FOR_INTERRUPTS(); 115 | Py_INCREF(Py_None); 116 | return Py_None; 117 | } 118 | 119 | 120 | #pragma GCC diagnostic push 121 | /* The CPython documentation requires the casting. */ 122 | #pragma GCC diagnostic ignored "-Wcast-function-type" 123 | static PyMethodDef UtilsMethods[] = { 124 | {"_log_to_postgres", (PyCFunction) log_to_postgres, METH_VARARGS | METH_KEYWORDS, "Log to postresql client"}, 125 | {"check_interrupts", (PyCFunction) py_check_interrupts, METH_VARARGS | METH_KEYWORDS, "Gives control back to PostgreSQL"}, 126 | {NULL, NULL, 0, NULL} 127 | }; 128 | #pragma GCC diagnostic pop 129 | 130 | static struct PyModuleDef moduledef = { 131 | PyModuleDef_HEAD_INIT, 132 | "multicorn._utils", 133 | NULL, 134 | sizeof(struct module_state), 135 | UtilsMethods, 136 | NULL, 137 | NULL, 138 | NULL, 139 | NULL 140 | }; 141 | 142 | #define INITERROR return NULL 143 | 144 | #pragma GCC diagnostic push 145 | /* Useless message. */ 146 | #pragma GCC diagnostic ignored "-Wmissing-prototypes" 147 | /* Presumably st is a placeholder for something? */ 148 | #pragma GCC diagnostic ignored "-Wunused-but-set-variable" 149 | PyObject * 150 | PyInit__utils(void) 151 | { 152 | PyObject *module = PyModule_Create(&moduledef); 153 | struct module_state *st; 154 | 155 | if (module == NULL) 156 | INITERROR; 157 | st = GETSTATE(module); 158 | 159 | return module; 160 | } 161 | #pragma GCC diagnostic pop 162 | -------------------------------------------------------------------------------- /test-3.10: -------------------------------------------------------------------------------- 1 | test-3.9 -------------------------------------------------------------------------------- /test-3.11: -------------------------------------------------------------------------------- 1 | test-3.9 -------------------------------------------------------------------------------- /test-3.12: -------------------------------------------------------------------------------- 1 | test-3.9 -------------------------------------------------------------------------------- /test-3.13: -------------------------------------------------------------------------------- 1 | test-3.9 -------------------------------------------------------------------------------- /test-3.9/expected/import_sqlalchemy.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | create or replace function create_foreign_server() returns void as $block$ 4 | DECLARE 5 | current_db varchar; 6 | BEGIN 7 | SELECT into current_db current_database(); 8 | EXECUTE $$ 9 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 10 | wrapper 'multicorn.sqlalchemyfdw.SqlAlchemyFdw', 11 | db_url 'postgresql://$$ || current_user || '@localhost/' || current_db || $$' 12 | ); 13 | $$; 14 | END; 15 | $block$ language plpgsql; 16 | select create_foreign_server(); 17 | create_foreign_server 18 | ----------------------- 19 | 20 | (1 row) 21 | 22 | CREATE SCHEMA local_schema; 23 | CREATE TABLE local_schema.t1 ( 24 | c1 int primary key, 25 | c2 text, 26 | c3 timestamp, 27 | c4 numeric 28 | ); 29 | CREATE TABLE local_schema.t2 ( 30 | c1 int, 31 | c2 text, 32 | c3 timestamp, 33 | c4 numeric 34 | ); 35 | CREATE TABLE local_schema.t3 ( 36 | c1 int, 37 | c2 text, 38 | c3 timestamp, 39 | c4 numeric 40 | ); 41 | CREATE SCHEMA remote_schema; 42 | IMPORT FOREIGN SCHEMA local_schema FROM SERVER multicorn_srv INTO remote_schema ; 43 | SELECT * FROM remote_schema.t1; 44 | c1 | c2 | c3 | c4 45 | ----+----+----+---- 46 | (0 rows) 47 | 48 | INSERT INTO remote_schema.t1 VALUES (1, '2', NULL, NULL); 49 | SELECT * FROM remote_schema.t1; 50 | c1 | c2 | c3 | c4 51 | ----+----+----+---- 52 | 1 | 2 | | 53 | (1 row) 54 | 55 | DROP SCHEMA remote_schema CASCADE; 56 | NOTICE: drop cascades to 3 other objects 57 | DETAIL: drop cascades to foreign table remote_schema.t1 58 | drop cascades to foreign table remote_schema.t2 59 | drop cascades to foreign table remote_schema.t3 60 | CREATE SCHEMA remote_schema; 61 | IMPORT FOREIGN SCHEMA local_schema LIMIT TO (t1) FROM SERVER multicorn_srv INTO remote_schema ; 62 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'remote_schema'; 63 | relname 64 | --------- 65 | t1 66 | (1 row) 67 | 68 | IMPORT FOREIGN SCHEMA local_schema EXCEPT (t1, t3) FROM SERVER multicorn_srv INTO remote_schema ; 69 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'remote_schema'; 70 | relname 71 | --------- 72 | t1 73 | t2 74 | (2 rows) 75 | 76 | DROP EXTENSION multicorn CASCADE; 77 | NOTICE: drop cascades to 3 other objects 78 | DETAIL: drop cascades to server multicorn_srv 79 | drop cascades to foreign table remote_schema.t1 80 | drop cascades to foreign table remote_schema.t2 81 | DROP SCHEMA local_schema CASCADE; 82 | NOTICE: drop cascades to 3 other objects 83 | DETAIL: drop cascades to table local_schema.t1 84 | drop cascades to table local_schema.t2 85 | drop cascades to table local_schema.t3 86 | DROP SCHEMA remote_schema CASCADE; 87 | -------------------------------------------------------------------------------- /test-3.9/expected/import_test.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE SCHEMA import_dest1; 7 | IMPORT FOREIGN SCHEMA import_source FROM SERVER multicorn_srv INTO import_dest1; 8 | NOTICE: IMPORT import_source FROM srv {} OPTIONS {} RESTRICTION: None [] 9 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1' ORDER BY relname; 10 | relname 11 | ------------------ 12 | imported_table_1 13 | imported_table_2 14 | imported_table_3 15 | (3 rows) 16 | 17 | DROP SCHEMA import_dest1 CASCADE; 18 | NOTICE: drop cascades to 3 other objects 19 | DETAIL: drop cascades to foreign table import_dest1.imported_table_1 20 | drop cascades to foreign table import_dest1.imported_table_2 21 | drop cascades to foreign table import_dest1.imported_table_3 22 | CREATE SCHEMA import_dest1; 23 | IMPORT FOREIGN SCHEMA import_source EXCEPT (imported_table_1, imported_table_3) FROM SERVER multicorn_srv INTO import_dest1; 24 | NOTICE: IMPORT import_source FROM srv {} OPTIONS {} RESTRICTION: except ['imported_table_1', 'imported_table_3'] 25 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1'; 26 | relname 27 | ------------------ 28 | imported_table_2 29 | (1 row) 30 | 31 | IMPORT FOREIGN SCHEMA import_source LIMIT TO (imported_table_1) FROM SERVER multicorn_srv INTO import_dest1; 32 | NOTICE: IMPORT import_source FROM srv {} OPTIONS {} RESTRICTION: limit ['imported_table_1'] 33 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1' ORDER BY relname; 34 | relname 35 | ------------------ 36 | imported_table_1 37 | imported_table_2 38 | (2 rows) 39 | 40 | DROP EXTENSION multicorn cascade; 41 | NOTICE: drop cascades to 3 other objects 42 | DETAIL: drop cascades to server multicorn_srv 43 | drop cascades to foreign table import_dest1.imported_table_2 44 | drop cascades to foreign table import_dest1.imported_table_1 45 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_column_options_test.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying options (prefix 'test'), 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1' 11 | ); 12 | select * from testmulticorn limit 1; 13 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 14 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 15 | NOTICE: Column test1 options: {'prefix': 'test'} 16 | NOTICE: [] 17 | NOTICE: ['test1', 'test2'] 18 | test1 | test2 19 | -----------+----------- 20 | test1 1 0 | test2 2 0 21 | (1 row) 22 | 23 | ALTER foreign table testmulticorn alter test1 options (set prefix 'test2'); 24 | select * from testmulticorn limit 1; 25 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 26 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 27 | NOTICE: Column test1 options: {'prefix': 'test2'} 28 | NOTICE: [] 29 | NOTICE: ['test1', 'test2'] 30 | test1 | test2 31 | -----------+----------- 32 | test1 1 0 | test2 2 0 33 | (1 row) 34 | 35 | ALTER foreign table testmulticorn alter test1 options (drop prefix); 36 | select * from testmulticorn limit 1; 37 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 38 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 39 | NOTICE: [] 40 | NOTICE: ['test1', 'test2'] 41 | test1 | test2 42 | -----------+----------- 43 | test1 1 0 | test2 2 0 44 | (1 row) 45 | 46 | ALTER foreign table testmulticorn alter test1 options (add prefix 'test3'); 47 | select * from testmulticorn limit 1; 48 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 49 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 50 | NOTICE: Column test1 options: {'prefix': 'test3'} 51 | NOTICE: [] 52 | NOTICE: ['test1', 'test2'] 53 | test1 | test2 54 | -----------+----------- 55 | test1 1 0 | test2 2 0 56 | (1 row) 57 | 58 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 59 | DROP EXTENSION multicorn cascade; 60 | NOTICE: drop cascades to 2 other objects 61 | DETAIL: drop cascades to server multicorn_srv 62 | drop cascades to foreign table testmulticorn 63 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_error_test.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | -- Test that the wrapper option is required on the server. 4 | CREATE server multicorn_srv foreign data wrapper multicorn; 5 | ERROR: The wrapper parameter is mandatory, specify a valid class name 6 | -- Test that the wrapper option cannot be altered on the table 7 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 8 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 9 | ); 10 | CREATE foreign table testmulticorn ( 11 | test1 character varying, 12 | test2 character varying 13 | ) server multicorn_srv options ( 14 | option1 'option1', 15 | wrapper 'multicorn.evilwrapper.EvilDataWrapper' 16 | ); 17 | ERROR: Cannot set the wrapper class on the table 18 | HINT: Set it on the server 19 | ALTER server multicorn_srv options (DROP wrapper); 20 | ERROR: The wrapper parameter is mandatory, specify a valid class name 21 | CREATE server multicorn_empty_srv foreign data wrapper multicorn; 22 | ERROR: The wrapper parameter is mandatory, specify a valid class name 23 | DROP EXTENSION multicorn cascade; 24 | NOTICE: drop cascades to server multicorn_srv 25 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_logger_test.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying, 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | test_type 'logger' 12 | ); 13 | -- Test "normal" usage 14 | select * from testmulticorn; 15 | NOTICE: [('option1', 'option1'), ('test_type', 'logger')] 16 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 17 | WARNING: An error is about to occur 18 | ERROR: An error occured 19 | DROP EXTENSION multicorn cascade; 20 | NOTICE: drop cascades to 2 other objects 21 | DETAIL: drop cascades to server multicorn_srv 22 | drop cascades to foreign table testmulticorn 23 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_planner_test.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | \i test-common/disable_jit.include 3 | DO $$ 4 | BEGIN 5 | IF current_setting('server_version_num')::bigint >= 110000 THEN 6 | SET jit = off; 7 | END IF; 8 | END; 9 | $$ LANGUAGE plpgsql; 10 | CREATE EXTENSION multicorn; 11 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 12 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 13 | ); 14 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 15 | -- Test for two thing: first, that when a low total row count, 16 | -- a full seq scan is used on a join. 17 | CREATE foreign table testmulticorn ( 18 | test1 character varying, 19 | test2 character varying 20 | ) server multicorn_srv options ( 21 | option1 'option1' 22 | ); 23 | explain select * from testmulticorn; 24 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 25 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 26 | QUERY PLAN 27 | ---------------------------------------------------------------------- 28 | Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) 29 | (1 row) 30 | 31 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 32 | QUERY PLAN 33 | ------------------------------------------------------------------------------------- 34 | Nested Loop (cost=20.00..806.05 rows=20 width=128) 35 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 36 | -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) 37 | -> Materialize (cost=10.00..400.10 rows=20 width=20) 38 | -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) 39 | (5 rows) 40 | 41 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 42 | QUERY PLAN 43 | ------------------------------------------------------------------------------------- 44 | Nested Loop Left Join (cost=20.00..806.05 rows=20 width=128) 45 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 46 | -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) 47 | -> Materialize (cost=10.00..400.10 rows=20 width=20) 48 | -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) 49 | (5 rows) 50 | 51 | DROP foreign table testmulticorn; 52 | -- Second, when a total row count is high 53 | -- a parameterized path is used on the test1 attribute. 54 | CREATE foreign table testmulticorn ( 55 | test1 character varying, 56 | test2 character varying 57 | ) server multicorn_srv options ( 58 | option1 'option1', 59 | test_type 'planner', 60 | noisy_explain 'true' 61 | ); 62 | explain select * from testmulticorn; 63 | NOTICE: [('noisy_explain', 'true'), ('option1', 'option1'), ('test_type', 'planner'), ('usermapping', 'test')] 64 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 65 | NOTICE: EXPLAIN quals=[] 66 | NOTICE: EXPLAIN columns=['test1', 'test2'] 67 | NOTICE: EXPLAIN sortkeys=None 68 | NOTICE: EXPLAIN verbose=False 69 | QUERY PLAN 70 | ---------------------------------------------------------------------------------- 71 | Foreign Scan on testmulticorn (cost=10.00..200000000.00 rows=10000000 width=20) 72 | Multicorn: EXPLAIN ROW 1 73 | Multicorn: EXPLAIN ROW 2 74 | (3 rows) 75 | 76 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 77 | NOTICE: EXPLAIN quals=[] 78 | NOTICE: EXPLAIN columns=['test1', 'test2'] 79 | NOTICE: EXPLAIN sortkeys=None 80 | NOTICE: EXPLAIN verbose=False 81 | NOTICE: EXPLAIN quals=[test1 = ?] 82 | NOTICE: EXPLAIN columns=['test1', 'test2'] 83 | NOTICE: EXPLAIN sortkeys=None 84 | NOTICE: EXPLAIN verbose=False 85 | QUERY PLAN 86 | ------------------------------------------------------------------------------------------- 87 | Nested Loop (cost=20.00..400100000.00 rows=500000000000 width=128) 88 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 89 | Multicorn: EXPLAIN ROW 1 90 | Multicorn: EXPLAIN ROW 2 91 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 92 | Filter: ((m1.test1)::text = (test1)::text) 93 | Multicorn: EXPLAIN ROW 1 94 | Multicorn: EXPLAIN ROW 2 95 | (8 rows) 96 | 97 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 98 | NOTICE: EXPLAIN quals=[] 99 | NOTICE: EXPLAIN columns=['test1', 'test2'] 100 | NOTICE: EXPLAIN sortkeys=None 101 | NOTICE: EXPLAIN verbose=False 102 | NOTICE: EXPLAIN quals=[test1 = ?] 103 | NOTICE: EXPLAIN columns=['test1', 'test2'] 104 | NOTICE: EXPLAIN sortkeys=None 105 | NOTICE: EXPLAIN verbose=False 106 | QUERY PLAN 107 | ------------------------------------------------------------------------------------------- 108 | Nested Loop Left Join (cost=20.00..400100000.00 rows=500000000000 width=128) 109 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 110 | Multicorn: EXPLAIN ROW 1 111 | Multicorn: EXPLAIN ROW 2 112 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 113 | Filter: ((m1.test1)::text = (test1)::text) 114 | Multicorn: EXPLAIN ROW 1 115 | Multicorn: EXPLAIN ROW 2 116 | (8 rows) 117 | 118 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on upper(m1.test1) = m2.test1; 119 | NOTICE: EXPLAIN quals=[] 120 | NOTICE: EXPLAIN columns=['test1', 'test2'] 121 | NOTICE: EXPLAIN sortkeys=None 122 | NOTICE: EXPLAIN verbose=False 123 | NOTICE: EXPLAIN quals=[test1 = ?] 124 | NOTICE: EXPLAIN columns=['test1', 'test2'] 125 | NOTICE: EXPLAIN sortkeys=None 126 | NOTICE: EXPLAIN verbose=False 127 | QUERY PLAN 128 | ------------------------------------------------------------------------------------------- 129 | Nested Loop Left Join (cost=20.00..400100000.00 rows=500000000000 width=128) 130 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 131 | Multicorn: EXPLAIN ROW 1 132 | Multicorn: EXPLAIN ROW 2 133 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 134 | Filter: (upper((m1.test1)::text) = (test1)::text) 135 | Multicorn: EXPLAIN ROW 1 136 | Multicorn: EXPLAIN ROW 2 137 | (8 rows) 138 | 139 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 140 | DROP EXTENSION multicorn cascade; 141 | NOTICE: drop cascades to 2 other objects 142 | DETAIL: drop cascades to server multicorn_srv 143 | drop cascades to foreign table testmulticorn 144 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_planner_test_1.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | \i test-common/disable_jit.include 3 | DO $$ 4 | BEGIN 5 | IF current_setting('server_version_num')::bigint >= 110000 THEN 6 | SET jit = off; 7 | END IF; 8 | END; 9 | $$ LANGUAGE plpgsql; 10 | CREATE EXTENSION multicorn; 11 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 12 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 13 | ); 14 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 15 | -- Test for two thing: first, that when a low total row count, 16 | -- a full seq scan is used on a join. 17 | CREATE foreign table testmulticorn ( 18 | test1 character varying, 19 | test2 character varying 20 | ) server multicorn_srv options ( 21 | option1 'option1' 22 | ); 23 | explain select * from testmulticorn; 24 | NOTICE: [('option1', 'option1'), ('usermapping', 'test')] 25 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 26 | QUERY PLAN 27 | ---------------------------------------------------------------------- 28 | Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) 29 | (1 row) 30 | 31 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 32 | QUERY PLAN 33 | ------------------------------------------------------------------------------------- 34 | Nested Loop (cost=20.00..806.05 rows=20 width=128) 35 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 36 | -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) 37 | -> Materialize (cost=10.00..400.10 rows=20 width=20) 38 | -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) 39 | (5 rows) 40 | 41 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 42 | QUERY PLAN 43 | ------------------------------------------------------------------------------------- 44 | Nested Loop Left Join (cost=20.00..806.05 rows=20 width=128) 45 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 46 | -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) 47 | -> Materialize (cost=10.00..400.10 rows=20 width=20) 48 | -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) 49 | (5 rows) 50 | 51 | DROP foreign table testmulticorn; 52 | -- Second, when a total row count is high 53 | -- a parameterized path is used on the test1 attribute. 54 | CREATE foreign table testmulticorn ( 55 | test1 character varying, 56 | test2 character varying 57 | ) server multicorn_srv options ( 58 | option1 'option1', 59 | test_type 'planner', 60 | noisy_explain 'true' 61 | ); 62 | explain select * from testmulticorn; 63 | NOTICE: [('noisy_explain', 'true'), ('option1', 'option1'), ('test_type', 'planner'), ('usermapping', 'test')] 64 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 65 | NOTICE: EXPLAIN quals=[] 66 | NOTICE: EXPLAIN columns=['test1', 'test2'] 67 | NOTICE: EXPLAIN sortkeys=None 68 | NOTICE: EXPLAIN verbose=False 69 | QUERY PLAN 70 | ---------------------------------------------------------------------------------- 71 | Foreign Scan on testmulticorn (cost=10.00..200000000.00 rows=10000000 width=20) 72 | Multicorn: EXPLAIN ROW 1 73 | Multicorn: EXPLAIN ROW 2 74 | (3 rows) 75 | 76 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 77 | NOTICE: EXPLAIN quals=[] 78 | NOTICE: EXPLAIN columns=['test1', 'test2'] 79 | NOTICE: EXPLAIN sortkeys=None 80 | NOTICE: EXPLAIN verbose=False 81 | NOTICE: EXPLAIN quals=[test1 = ?] 82 | NOTICE: EXPLAIN columns=['test1', 'test2'] 83 | NOTICE: EXPLAIN sortkeys=None 84 | NOTICE: EXPLAIN verbose=False 85 | QUERY PLAN 86 | ------------------------------------------------------------------------------------------- 87 | Nested Loop (cost=20.00..400125000.00 rows=500000000000 width=128) 88 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 89 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 90 | Multicorn: EXPLAIN ROW 1 91 | Multicorn: EXPLAIN ROW 2 92 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 93 | Filter: ((m1.test1)::text = (test1)::text) 94 | Multicorn: EXPLAIN ROW 1 95 | Multicorn: EXPLAIN ROW 2 96 | (9 rows) 97 | 98 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 99 | NOTICE: EXPLAIN quals=[] 100 | NOTICE: EXPLAIN columns=['test1', 'test2'] 101 | NOTICE: EXPLAIN sortkeys=None 102 | NOTICE: EXPLAIN verbose=False 103 | NOTICE: EXPLAIN quals=[test1 = ?] 104 | NOTICE: EXPLAIN columns=['test1', 'test2'] 105 | NOTICE: EXPLAIN sortkeys=None 106 | NOTICE: EXPLAIN verbose=False 107 | QUERY PLAN 108 | ------------------------------------------------------------------------------------------- 109 | Nested Loop Left Join (cost=20.00..400125000.00 rows=500000000000 width=128) 110 | Join Filter: ((m1.test1)::text = (m2.test1)::text) 111 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 112 | Multicorn: EXPLAIN ROW 1 113 | Multicorn: EXPLAIN ROW 2 114 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 115 | Filter: ((m1.test1)::text = (test1)::text) 116 | Multicorn: EXPLAIN ROW 1 117 | Multicorn: EXPLAIN ROW 2 118 | (9 rows) 119 | 120 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on upper(m1.test1) = m2.test1; 121 | NOTICE: EXPLAIN quals=[] 122 | NOTICE: EXPLAIN columns=['test1', 'test2'] 123 | NOTICE: EXPLAIN sortkeys=None 124 | NOTICE: EXPLAIN verbose=False 125 | NOTICE: EXPLAIN quals=[test1 = ?] 126 | NOTICE: EXPLAIN columns=['test1', 'test2'] 127 | NOTICE: EXPLAIN sortkeys=None 128 | NOTICE: EXPLAIN verbose=False 129 | QUERY PLAN 130 | ------------------------------------------------------------------------------------------- 131 | Nested Loop Left Join (cost=20.00..400150000.00 rows=500000000000 width=128) 132 | Join Filter: (upper((m1.test1)::text) = (m2.test1)::text) 133 | -> Foreign Scan on testmulticorn m1 (cost=10.00..200000000.00 rows=10000000 width=20) 134 | Multicorn: EXPLAIN ROW 1 135 | Multicorn: EXPLAIN ROW 2 136 | -> Foreign Scan on testmulticorn m2 (cost=10.00..20.00 rows=1 width=20) 137 | Filter: (upper((m1.test1)::text) = (test1)::text) 138 | Multicorn: EXPLAIN ROW 1 139 | Multicorn: EXPLAIN ROW 2 140 | (9 rows) 141 | 142 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 143 | DROP EXTENSION multicorn cascade; 144 | NOTICE: drop cascades to 2 other objects 145 | DETAIL: drop cascades to server multicorn_srv 146 | drop cascades to foreign table testmulticorn 147 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_test_date.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | CREATE foreign table testmulticorn ( 8 | test1 date, 9 | test2 timestamp 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'date' 13 | ); 14 | -- Test "normal" usage 15 | select * from testmulticorn; 16 | NOTICE: [('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] 17 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 18 | NOTICE: [] 19 | NOTICE: ['test1', 'test2'] 20 | test1 | test2 21 | ------------+-------------------------- 22 | 01-01-2011 | Sun Jan 02 14:30:25 2011 23 | 02-03-2011 | Tue Feb 01 14:30:25 2011 24 | 03-02-2011 | Thu Mar 03 14:30:25 2011 25 | 04-01-2011 | Sat Apr 02 14:30:25 2011 26 | 05-03-2011 | Sun May 01 14:30:25 2011 27 | 06-02-2011 | Fri Jun 03 14:30:25 2011 28 | 07-01-2011 | Sat Jul 02 14:30:25 2011 29 | 08-03-2011 | Mon Aug 01 14:30:25 2011 30 | 09-02-2011 | Sat Sep 03 14:30:25 2011 31 | 10-01-2011 | Sun Oct 02 14:30:25 2011 32 | 11-03-2011 | Tue Nov 01 14:30:25 2011 33 | 12-02-2011 | Sat Dec 03 14:30:25 2011 34 | 01-01-2011 | Sun Jan 02 14:30:25 2011 35 | 02-03-2011 | Tue Feb 01 14:30:25 2011 36 | 03-02-2011 | Thu Mar 03 14:30:25 2011 37 | 04-01-2011 | Sat Apr 02 14:30:25 2011 38 | 05-03-2011 | Sun May 01 14:30:25 2011 39 | 06-02-2011 | Fri Jun 03 14:30:25 2011 40 | 07-01-2011 | Sat Jul 02 14:30:25 2011 41 | 08-03-2011 | Mon Aug 01 14:30:25 2011 42 | (20 rows) 43 | 44 | select * from testmulticorn where test1 < '2011-06-01'; 45 | NOTICE: [test1 < 2011-06-01] 46 | NOTICE: ['test1', 'test2'] 47 | test1 | test2 48 | ------------+-------------------------- 49 | 01-01-2011 | Sun Jan 02 14:30:25 2011 50 | 02-03-2011 | Tue Feb 01 14:30:25 2011 51 | 03-02-2011 | Thu Mar 03 14:30:25 2011 52 | 04-01-2011 | Sat Apr 02 14:30:25 2011 53 | 05-03-2011 | Sun May 01 14:30:25 2011 54 | 01-01-2011 | Sun Jan 02 14:30:25 2011 55 | 02-03-2011 | Tue Feb 01 14:30:25 2011 56 | 03-02-2011 | Thu Mar 03 14:30:25 2011 57 | 04-01-2011 | Sat Apr 02 14:30:25 2011 58 | 05-03-2011 | Sun May 01 14:30:25 2011 59 | (10 rows) 60 | 61 | select * from testmulticorn where test2 < '2011-06-01 00:00:00'; 62 | NOTICE: [test2 < 2011-06-01 00:00:00] 63 | NOTICE: ['test1', 'test2'] 64 | test1 | test2 65 | ------------+-------------------------- 66 | 01-01-2011 | Sun Jan 02 14:30:25 2011 67 | 02-03-2011 | Tue Feb 01 14:30:25 2011 68 | 03-02-2011 | Thu Mar 03 14:30:25 2011 69 | 04-01-2011 | Sat Apr 02 14:30:25 2011 70 | 05-03-2011 | Sun May 01 14:30:25 2011 71 | 01-01-2011 | Sun Jan 02 14:30:25 2011 72 | 02-03-2011 | Tue Feb 01 14:30:25 2011 73 | 03-02-2011 | Thu Mar 03 14:30:25 2011 74 | 04-01-2011 | Sat Apr 02 14:30:25 2011 75 | 05-03-2011 | Sun May 01 14:30:25 2011 76 | (10 rows) 77 | 78 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 79 | DROP EXTENSION multicorn cascade; 80 | NOTICE: drop cascades to 2 other objects 81 | DETAIL: drop cascades to server multicorn_srv 82 | drop cascades to foreign table testmulticorn 83 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_test_dict.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE EXTENSION hstore; 4 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 5 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 6 | ); 7 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 8 | CREATE foreign table testmulticorn ( 9 | test1 hstore, 10 | test2 hstore 11 | ) server multicorn_srv options ( 12 | option1 'option1', 13 | test_type 'dict' 14 | ); 15 | -- Test "normal" usage 16 | select * from testmulticorn; 17 | NOTICE: [('option1', 'option1'), ('test_type', 'dict'), ('usermapping', 'test')] 18 | NOTICE: [('test1', 'hstore'), ('test2', 'hstore')] 19 | NOTICE: [] 20 | NOTICE: ['test1', 'test2'] 21 | test1 | test2 22 | ----------------------------------------------------------------------------------+---------------------------------------------------------------------------------- 23 | "index"=>"0", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"0", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 24 | "index"=>"1", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"1", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 25 | "index"=>"2", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"2", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 26 | "index"=>"3", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"3", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 27 | "index"=>"4", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"4", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 28 | "index"=>"5", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"5", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 29 | "index"=>"6", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"6", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 30 | "index"=>"7", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"7", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 31 | "index"=>"8", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"8", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 32 | "index"=>"9", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"9", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 33 | "index"=>"10", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"10", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 34 | "index"=>"11", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"11", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 35 | "index"=>"12", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"12", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 36 | "index"=>"13", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"13", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 37 | "index"=>"14", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"14", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 38 | "index"=>"15", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"15", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 39 | "index"=>"16", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"16", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 40 | "index"=>"17", "repeater"=>"2", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"17", "repeater"=>"3", "column_name"=>"test2", "maybe_hstore"=>"a => b" 41 | "index"=>"18", "repeater"=>"1", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"18", "repeater"=>"2", "column_name"=>"test2", "maybe_hstore"=>"a => b" 42 | "index"=>"19", "repeater"=>"3", "column_name"=>"test1", "maybe_hstore"=>"a => b" | "index"=>"19", "repeater"=>"1", "column_name"=>"test2", "maybe_hstore"=>"a => b" 43 | (20 rows) 44 | 45 | select test1 -> 'repeater' as r from testmulticorn order by r; 46 | NOTICE: [] 47 | NOTICE: ['test1'] 48 | r 49 | --- 50 | 1 51 | 1 52 | 1 53 | 1 54 | 1 55 | 1 56 | 1 57 | 2 58 | 2 59 | 2 60 | 2 61 | 2 62 | 2 63 | 3 64 | 3 65 | 3 66 | 3 67 | 3 68 | 3 69 | 3 70 | (20 rows) 71 | 72 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 73 | DROP EXTENSION multicorn cascade; 74 | NOTICE: drop cascades to 2 other objects 75 | DETAIL: drop cascades to server multicorn_srv 76 | drop cascades to foreign table testmulticorn 77 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_test_list.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying[], 9 | test2 character varying[] 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'list' 13 | ); 14 | -- Test "normal" usage 15 | select * from testmulticorn; 16 | NOTICE: [('option1', 'option1'), ('test_type', 'list'), ('usermapping', 'test')] 17 | NOTICE: [('test1', 'character varying[]'), ('test2', 'character varying[]')] 18 | NOTICE: [] 19 | NOTICE: ['test1', 'test2'] 20 | test1 | test2 21 | ------------------------------------------------------+------------------------------------------------------ 22 | {test1,1,0,"test1,\"0\"","{some value, \\\" ' 2}"} | {test2,2,0,"test2,\"0\"","{some value, \\\" ' 2}"} 23 | {test1,3,1,"test1,\"1\"","{some value, \\\" ' 2}"} | {test2,1,1,"test2,\"1\"","{some value, \\\" ' 2}"} 24 | {test1,2,2,"test1,\"2\"","{some value, \\\" ' 2}"} | {test2,3,2,"test2,\"2\"","{some value, \\\" ' 2}"} 25 | {test1,1,3,"test1,\"3\"","{some value, \\\" ' 2}"} | {test2,2,3,"test2,\"3\"","{some value, \\\" ' 2}"} 26 | {test1,3,4,"test1,\"4\"","{some value, \\\" ' 2}"} | {test2,1,4,"test2,\"4\"","{some value, \\\" ' 2}"} 27 | {test1,2,5,"test1,\"5\"","{some value, \\\" ' 2}"} | {test2,3,5,"test2,\"5\"","{some value, \\\" ' 2}"} 28 | {test1,1,6,"test1,\"6\"","{some value, \\\" ' 2}"} | {test2,2,6,"test2,\"6\"","{some value, \\\" ' 2}"} 29 | {test1,3,7,"test1,\"7\"","{some value, \\\" ' 2}"} | {test2,1,7,"test2,\"7\"","{some value, \\\" ' 2}"} 30 | {test1,2,8,"test1,\"8\"","{some value, \\\" ' 2}"} | {test2,3,8,"test2,\"8\"","{some value, \\\" ' 2}"} 31 | {test1,1,9,"test1,\"9\"","{some value, \\\" ' 2}"} | {test2,2,9,"test2,\"9\"","{some value, \\\" ' 2}"} 32 | {test1,3,10,"test1,\"10\"","{some value, \\\" ' 2}"} | {test2,1,10,"test2,\"10\"","{some value, \\\" ' 2}"} 33 | {test1,2,11,"test1,\"11\"","{some value, \\\" ' 2}"} | {test2,3,11,"test2,\"11\"","{some value, \\\" ' 2}"} 34 | {test1,1,12,"test1,\"12\"","{some value, \\\" ' 2}"} | {test2,2,12,"test2,\"12\"","{some value, \\\" ' 2}"} 35 | {test1,3,13,"test1,\"13\"","{some value, \\\" ' 2}"} | {test2,1,13,"test2,\"13\"","{some value, \\\" ' 2}"} 36 | {test1,2,14,"test1,\"14\"","{some value, \\\" ' 2}"} | {test2,3,14,"test2,\"14\"","{some value, \\\" ' 2}"} 37 | {test1,1,15,"test1,\"15\"","{some value, \\\" ' 2}"} | {test2,2,15,"test2,\"15\"","{some value, \\\" ' 2}"} 38 | {test1,3,16,"test1,\"16\"","{some value, \\\" ' 2}"} | {test2,1,16,"test2,\"16\"","{some value, \\\" ' 2}"} 39 | {test1,2,17,"test1,\"17\"","{some value, \\\" ' 2}"} | {test2,3,17,"test2,\"17\"","{some value, \\\" ' 2}"} 40 | {test1,1,18,"test1,\"18\"","{some value, \\\" ' 2}"} | {test2,2,18,"test2,\"18\"","{some value, \\\" ' 2}"} 41 | {test1,3,19,"test1,\"19\"","{some value, \\\" ' 2}"} | {test2,1,19,"test2,\"19\"","{some value, \\\" ' 2}"} 42 | (20 rows) 43 | 44 | select test1[2] as c from testmulticorn order by c; 45 | NOTICE: [] 46 | NOTICE: ['test1'] 47 | c 48 | --- 49 | 1 50 | 1 51 | 1 52 | 1 53 | 1 54 | 1 55 | 1 56 | 2 57 | 2 58 | 2 59 | 2 60 | 2 61 | 2 62 | 3 63 | 3 64 | 3 65 | 3 66 | 3 67 | 3 68 | 3 69 | (20 rows) 70 | 71 | alter foreign table testmulticorn alter test1 type varchar; 72 | select * from testmulticorn; 73 | NOTICE: [('option1', 'option1'), ('test_type', 'list'), ('usermapping', 'test')] 74 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying[]')] 75 | NOTICE: [] 76 | NOTICE: ['test1', 'test2'] 77 | test1 | test2 78 | ----------------------------------------------------------+------------------------------------------------------ 79 | ['test1', 1, 0, 'test1,"0"', '{some value, \\" \' 2}'] | {test2,2,0,"test2,\"0\"","{some value, \\\" ' 2}"} 80 | ['test1', 3, 1, 'test1,"1"', '{some value, \\" \' 2}'] | {test2,1,1,"test2,\"1\"","{some value, \\\" ' 2}"} 81 | ['test1', 2, 2, 'test1,"2"', '{some value, \\" \' 2}'] | {test2,3,2,"test2,\"2\"","{some value, \\\" ' 2}"} 82 | ['test1', 1, 3, 'test1,"3"', '{some value, \\" \' 2}'] | {test2,2,3,"test2,\"3\"","{some value, \\\" ' 2}"} 83 | ['test1', 3, 4, 'test1,"4"', '{some value, \\" \' 2}'] | {test2,1,4,"test2,\"4\"","{some value, \\\" ' 2}"} 84 | ['test1', 2, 5, 'test1,"5"', '{some value, \\" \' 2}'] | {test2,3,5,"test2,\"5\"","{some value, \\\" ' 2}"} 85 | ['test1', 1, 6, 'test1,"6"', '{some value, \\" \' 2}'] | {test2,2,6,"test2,\"6\"","{some value, \\\" ' 2}"} 86 | ['test1', 3, 7, 'test1,"7"', '{some value, \\" \' 2}'] | {test2,1,7,"test2,\"7\"","{some value, \\\" ' 2}"} 87 | ['test1', 2, 8, 'test1,"8"', '{some value, \\" \' 2}'] | {test2,3,8,"test2,\"8\"","{some value, \\\" ' 2}"} 88 | ['test1', 1, 9, 'test1,"9"', '{some value, \\" \' 2}'] | {test2,2,9,"test2,\"9\"","{some value, \\\" ' 2}"} 89 | ['test1', 3, 10, 'test1,"10"', '{some value, \\" \' 2}'] | {test2,1,10,"test2,\"10\"","{some value, \\\" ' 2}"} 90 | ['test1', 2, 11, 'test1,"11"', '{some value, \\" \' 2}'] | {test2,3,11,"test2,\"11\"","{some value, \\\" ' 2}"} 91 | ['test1', 1, 12, 'test1,"12"', '{some value, \\" \' 2}'] | {test2,2,12,"test2,\"12\"","{some value, \\\" ' 2}"} 92 | ['test1', 3, 13, 'test1,"13"', '{some value, \\" \' 2}'] | {test2,1,13,"test2,\"13\"","{some value, \\\" ' 2}"} 93 | ['test1', 2, 14, 'test1,"14"', '{some value, \\" \' 2}'] | {test2,3,14,"test2,\"14\"","{some value, \\\" ' 2}"} 94 | ['test1', 1, 15, 'test1,"15"', '{some value, \\" \' 2}'] | {test2,2,15,"test2,\"15\"","{some value, \\\" ' 2}"} 95 | ['test1', 3, 16, 'test1,"16"', '{some value, \\" \' 2}'] | {test2,1,16,"test2,\"16\"","{some value, \\\" ' 2}"} 96 | ['test1', 2, 17, 'test1,"17"', '{some value, \\" \' 2}'] | {test2,3,17,"test2,\"17\"","{some value, \\\" ' 2}"} 97 | ['test1', 1, 18, 'test1,"18"', '{some value, \\" \' 2}'] | {test2,2,18,"test2,\"18\"","{some value, \\\" ' 2}"} 98 | ['test1', 3, 19, 'test1,"19"', '{some value, \\" \' 2}'] | {test2,1,19,"test2,\"19\"","{some value, \\\" ' 2}"} 99 | (20 rows) 100 | 101 | alter foreign table testmulticorn options (set test_type 'nested_list'); 102 | select * from testmulticorn limit 1; 103 | NOTICE: [('option1', 'option1'), ('test_type', 'nested_list'), ('usermapping', 'test')] 104 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying[]')] 105 | NOTICE: [] 106 | NOTICE: ['test1', 'test2'] 107 | test1 | test2 108 | --------------------------------------------------------------------+----------------------------------------------------------------------------- 109 | [['test1', 'test1'], [1, '{some value, \\" 2}'], [0, 'test1,"0"']] | {"['test2', 'test2']","[2, '{some value, \\\\\" 2}']","[0, 'test2,\"0\"']"} 110 | (1 row) 111 | 112 | alter foreign table testmulticorn alter test1 type varchar[]; 113 | alter foreign table testmulticorn alter test2 type varchar[][]; 114 | select test1[2], test2[2][2], array_length(test1, 1), array_length(test2, 1), array_length(test2, 2) from testmulticorn limit 1; 115 | NOTICE: [('option1', 'option1'), ('test_type', 'nested_list'), ('usermapping', 'test')] 116 | NOTICE: [('test1', 'character varying[]'), ('test2', 'character varying[]')] 117 | NOTICE: [] 118 | NOTICE: ['test1', 'test2'] 119 | test1 | test2 | array_length | array_length | array_length 120 | ----------------------------+--------------------+--------------+--------------+-------------- 121 | [1, '{some value, \\" 2}'] | {some value, \" 2} | 3 | 3 | 2 122 | (1 row) 123 | 124 | select length(test1[2]) from testmulticorn limit 1; 125 | NOTICE: [] 126 | NOTICE: ['test1'] 127 | length 128 | -------- 129 | 26 130 | (1 row) 131 | 132 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 133 | DROP EXTENSION multicorn cascade; 134 | NOTICE: drop cascades to 2 other objects 135 | DETAIL: drop cascades to server multicorn_srv 136 | drop cascades to foreign table testmulticorn 137 | -------------------------------------------------------------------------------- /test-3.9/expected/multicorn_test_sort.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | CREATE foreign table testmulticorn ( 8 | test1 date, 9 | test2 timestamp 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'date' 13 | ); 14 | -- Test sort pushdown asked 15 | EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 DESC; 16 | NOTICE: [('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] 17 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 18 | QUERY PLAN 19 | ---------------------------------------------------------------------- 20 | Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) 21 | (1 row) 22 | 23 | -- Data should be sorted 24 | SELECT * FROM testmulticorn ORDER BY test1 DESC; 25 | NOTICE: [] 26 | NOTICE: ['test1', 'test2'] 27 | NOTICE: requested sort(s): 28 | NOTICE: SortKey(attname='test1', attnum=1, is_reversed=True, nulls_first=True, collate=None) 29 | test1 | test2 30 | ------------+-------------------------- 31 | 12-02-2011 | Sat Dec 03 14:30:25 2011 32 | 11-03-2011 | Tue Nov 01 14:30:25 2011 33 | 10-01-2011 | Sun Oct 02 14:30:25 2011 34 | 09-02-2011 | Sat Sep 03 14:30:25 2011 35 | 08-03-2011 | Mon Aug 01 14:30:25 2011 36 | 08-03-2011 | Mon Aug 01 14:30:25 2011 37 | 07-01-2011 | Sat Jul 02 14:30:25 2011 38 | 07-01-2011 | Sat Jul 02 14:30:25 2011 39 | 06-02-2011 | Fri Jun 03 14:30:25 2011 40 | 06-02-2011 | Fri Jun 03 14:30:25 2011 41 | 05-03-2011 | Sun May 01 14:30:25 2011 42 | 05-03-2011 | Sun May 01 14:30:25 2011 43 | 04-01-2011 | Sat Apr 02 14:30:25 2011 44 | 04-01-2011 | Sat Apr 02 14:30:25 2011 45 | 03-02-2011 | Thu Mar 03 14:30:25 2011 46 | 03-02-2011 | Thu Mar 03 14:30:25 2011 47 | 02-03-2011 | Tue Feb 01 14:30:25 2011 48 | 02-03-2011 | Tue Feb 01 14:30:25 2011 49 | 01-01-2011 | Sun Jan 02 14:30:25 2011 50 | 01-01-2011 | Sun Jan 02 14:30:25 2011 51 | (20 rows) 52 | 53 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 54 | DROP EXTENSION multicorn cascade; 55 | NOTICE: drop cascades to 2 other objects 56 | DETAIL: drop cascades to server multicorn_srv 57 | drop cascades to foreign table testmulticorn 58 | -------------------------------------------------------------------------------- /test-3.9/expected/write_batch_test.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn_write( 7 | fname1 character varying, 8 | fname2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | row_id_column 'test1', 12 | modify_batch_size '2' 13 | ); 14 | insert into testmulticorn_write(fname1, fname2) 15 | VALUES 16 | ('col1_val1', 'col2_val'), 17 | ('col1_val2', 'col2_val'), 18 | ('col1_val3', 'col2_val'), 19 | ('col1_val4', 'col2_val'), 20 | ('col1_val5', 'col2_val'); 21 | NOTICE: [('modify_batch_size', '2'), ('option1', 'option1'), ('row_id_column', 'test1'), ('usermapping', 'test')] 22 | NOTICE: [('fname1', 'character varying'), ('fname2', 'character varying')] 23 | NOTICE: BULK INSERTING: [[('fname1', 'col1_val1'), ('fname2', 'col2_val')], [('fname1', 'col1_val2'), ('fname2', 'col2_val')]] 24 | NOTICE: BULK INSERTING: [[('fname1', 'col1_val3'), ('fname2', 'col2_val')], [('fname1', 'col1_val4'), ('fname2', 'col2_val')]] 25 | NOTICE: BULK INSERTING: [[('fname1', 'col1_val5'), ('fname2', 'col2_val')]] 26 | CREATE OR REPLACE FUNCTION notify_insert() 27 | RETURNS TRIGGER AS $$ 28 | BEGIN 29 | RAISE NOTICE 'Inserted Record: %, %', NEW.fname1, NEW.fname2; 30 | RETURN NEW; 31 | END; 32 | $$ LANGUAGE plpgsql; 33 | CREATE TRIGGER after_insert_trigger 34 | AFTER INSERT ON testmulticorn_write 35 | FOR EACH ROW 36 | EXECUTE FUNCTION notify_insert(); 37 | INSERT INTO testmulticorn_write(fname1, fname2) 38 | VALUES 39 | ('col1_val6', 'col2_val'), 40 | ('col1_val7', 'col2_val'); 41 | NOTICE: BULK INSERTING: [[('fname1', 'col1_val6'), ('fname2', 'col2_val')], [('fname1', 'col1_val7'), ('fname2', 'col2_val')]] 42 | NOTICE: Inserted Record: col1_val6, col2_val 43 | NOTICE: Inserted Record: col1_val7, col2_val 44 | DROP TRIGGER after_insert_trigger ON testmulticorn_write; 45 | DROP FUNCTION notify_insert(); 46 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 47 | DROP EXTENSION multicorn cascade; 48 | NOTICE: drop cascades to 2 other objects 49 | DETAIL: drop cascades to server multicorn_srv 50 | drop cascades to foreign table testmulticorn_write 51 | -------------------------------------------------------------------------------- /test-3.9/expected/write_savepoints.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying, 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | test_type 'nowrite' 12 | ); 13 | -- No savepoints 14 | BEGIN; 15 | CREATE foreign table testmulticorn_write ( 16 | test1 character varying, 17 | test2 character varying 18 | ) server multicorn_srv options ( 19 | option1 'option1', 20 | row_id_column 'test1', 21 | test_type 'returning' 22 | ); 23 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 24 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 25 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 26 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 27 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 28 | NOTICE: [test1 = 0] 29 | NOTICE: ['test1'] 30 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 31 | NOTICE: [test1 = 1] 32 | NOTICE: ['test1'] 33 | delete from testmulticorn_write where test1 = '1'; 34 | NOTICE: [test1 = 1] 35 | NOTICE: ['test1'] 36 | DROP foreign table testmulticorn_write; 37 | ROLLBACK; 38 | -- One savepoint 39 | BEGIN; 40 | CREATE foreign table testmulticorn_write ( 41 | test1 character varying, 42 | test2 character varying 43 | ) server multicorn_srv options ( 44 | option1 'option1', 45 | row_id_column 'test1', 46 | test_type 'returning' 47 | ); 48 | SAVEPOINT A; 49 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 50 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 51 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 52 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 53 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 54 | NOTICE: [test1 = 0] 55 | NOTICE: ['test1'] 56 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 57 | NOTICE: [test1 = 1] 58 | NOTICE: ['test1'] 59 | delete from testmulticorn_write where test1 = '1'; 60 | NOTICE: [test1 = 1] 61 | NOTICE: ['test1'] 62 | ROLLBACK TO A; 63 | RELEASE A; 64 | DROP foreign table testmulticorn_write; 65 | COMMIT; 66 | -- Multiple sequential savepoints 67 | BEGIN; 68 | CREATE foreign table testmulticorn_write ( 69 | test1 character varying, 70 | test2 character varying 71 | ) server multicorn_srv options ( 72 | option1 'option1', 73 | row_id_column 'test1', 74 | test_type 'returning' 75 | ); 76 | SAVEPOINT A; 77 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 78 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 79 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 80 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 81 | select * from testmulticorn LIMIT 1; 82 | NOTICE: [('option1', 'option1'), ('test_type', 'nowrite'), ('usermapping', 'test')] 83 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 84 | NOTICE: [] 85 | NOTICE: ['test1', 'test2'] 86 | test1 | test2 87 | -----------+----------- 88 | test1 1 0 | test2 2 0 89 | (1 row) 90 | 91 | ROLLBACK TO A; 92 | RELEASE A; 93 | SAVEPOINT B; 94 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 95 | NOTICE: [test1 = 0] 96 | NOTICE: ['test1'] 97 | RELEASE B; 98 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 99 | NOTICE: [test1 = 1] 100 | NOTICE: ['test1'] 101 | delete from testmulticorn_write where test1 = '1'; 102 | NOTICE: [test1 = 1] 103 | NOTICE: ['test1'] 104 | DROP foreign table testmulticorn_write; 105 | ROLLBACK; 106 | -- Multiple nested savepoints 107 | BEGIN; 108 | CREATE foreign table testmulticorn_write ( 109 | test1 character varying, 110 | test2 character varying 111 | ) server multicorn_srv options ( 112 | option1 'option1', 113 | row_id_column 'test1', 114 | test_type 'returning' 115 | ); 116 | SAVEPOINT A; 117 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 118 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 119 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 120 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 121 | select * from testmulticorn LIMIT 1; 122 | NOTICE: [] 123 | NOTICE: ['test1', 'test2'] 124 | test1 | test2 125 | -----------+----------- 126 | test1 1 0 | test2 2 0 127 | (1 row) 128 | 129 | SAVEPOINT B; 130 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 131 | NOTICE: [test1 = 0] 132 | NOTICE: ['test1'] 133 | RELEASE B; 134 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 135 | NOTICE: [test1 = 1] 136 | NOTICE: ['test1'] 137 | delete from testmulticorn_write where test1 = '1'; 138 | NOTICE: [test1 = 1] 139 | NOTICE: ['test1'] 140 | ROLLBACK TO A; 141 | RELEASE A; 142 | DROP foreign table testmulticorn_write; 143 | ROLLBACK; 144 | -- Clean up 145 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 146 | DROP EXTENSION multicorn cascade; 147 | NOTICE: drop cascades to 2 other objects 148 | DETAIL: drop cascades to server multicorn_srv 149 | drop cascades to foreign table testmulticorn 150 | -------------------------------------------------------------------------------- /test-3.9/expected/write_savepoints_1.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying, 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | test_type 'nowrite' 12 | ); 13 | -- No savepoints 14 | BEGIN; 15 | CREATE foreign table testmulticorn_write ( 16 | test1 character varying, 17 | test2 character varying 18 | ) server multicorn_srv options ( 19 | option1 'option1', 20 | row_id_column 'test1', 21 | test_type 'returning' 22 | ); 23 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 24 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 25 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 26 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 27 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 28 | NOTICE: [test1 = 0] 29 | NOTICE: ['test1', 'test2'] 30 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 31 | NOTICE: [test1 = 1] 32 | NOTICE: ['test1', 'test2'] 33 | delete from testmulticorn_write where test1 = '1'; 34 | NOTICE: [test1 = 1] 35 | NOTICE: ['test1'] 36 | DROP foreign table testmulticorn_write; 37 | ROLLBACK; 38 | -- One savepoint 39 | BEGIN; 40 | CREATE foreign table testmulticorn_write ( 41 | test1 character varying, 42 | test2 character varying 43 | ) server multicorn_srv options ( 44 | option1 'option1', 45 | row_id_column 'test1', 46 | test_type 'returning' 47 | ); 48 | SAVEPOINT A; 49 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 50 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 51 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 52 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 53 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 54 | NOTICE: [test1 = 0] 55 | NOTICE: ['test1', 'test2'] 56 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 57 | NOTICE: [test1 = 1] 58 | NOTICE: ['test1', 'test2'] 59 | delete from testmulticorn_write where test1 = '1'; 60 | NOTICE: [test1 = 1] 61 | NOTICE: ['test1'] 62 | ROLLBACK TO A; 63 | RELEASE A; 64 | DROP foreign table testmulticorn_write; 65 | COMMIT; 66 | -- Multiple sequential savepoints 67 | BEGIN; 68 | CREATE foreign table testmulticorn_write ( 69 | test1 character varying, 70 | test2 character varying 71 | ) server multicorn_srv options ( 72 | option1 'option1', 73 | row_id_column 'test1', 74 | test_type 'returning' 75 | ); 76 | SAVEPOINT A; 77 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 78 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 79 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 80 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 81 | select * from testmulticorn LIMIT 1; 82 | NOTICE: [('option1', 'option1'), ('test_type', 'nowrite'), ('usermapping', 'test')] 83 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 84 | NOTICE: [] 85 | NOTICE: ['test1', 'test2'] 86 | test1 | test2 87 | -----------+----------- 88 | test1 1 0 | test2 2 0 89 | (1 row) 90 | 91 | ROLLBACK TO A; 92 | RELEASE A; 93 | SAVEPOINT B; 94 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 95 | NOTICE: [test1 = 0] 96 | NOTICE: ['test1', 'test2'] 97 | RELEASE B; 98 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 99 | NOTICE: [test1 = 1] 100 | NOTICE: ['test1', 'test2'] 101 | delete from testmulticorn_write where test1 = '1'; 102 | NOTICE: [test1 = 1] 103 | NOTICE: ['test1'] 104 | DROP foreign table testmulticorn_write; 105 | ROLLBACK; 106 | -- Multiple nested savepoints 107 | BEGIN; 108 | CREATE foreign table testmulticorn_write ( 109 | test1 character varying, 110 | test2 character varying 111 | ) server multicorn_srv options ( 112 | option1 'option1', 113 | row_id_column 'test1', 114 | test_type 'returning' 115 | ); 116 | SAVEPOINT A; 117 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 118 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('usermapping', 'test')] 119 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 120 | NOTICE: INSERTING: [('test1', '0'), ('test2', 'A')] 121 | select * from testmulticorn LIMIT 1; 122 | NOTICE: [] 123 | NOTICE: ['test1', 'test2'] 124 | test1 | test2 125 | -----------+----------- 126 | test1 1 0 | test2 2 0 127 | (1 row) 128 | 129 | SAVEPOINT B; 130 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 131 | NOTICE: [test1 = 0] 132 | NOTICE: ['test1', 'test2'] 133 | RELEASE B; 134 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 135 | NOTICE: [test1 = 1] 136 | NOTICE: ['test1', 'test2'] 137 | delete from testmulticorn_write where test1 = '1'; 138 | NOTICE: [test1 = 1] 139 | NOTICE: ['test1'] 140 | ROLLBACK TO A; 141 | RELEASE A; 142 | DROP foreign table testmulticorn_write; 143 | ROLLBACK; 144 | -- Clean up 145 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 146 | DROP EXTENSION multicorn cascade; 147 | NOTICE: drop cascades to 2 other objects 148 | DETAIL: drop cascades to server multicorn_srv 149 | drop cascades to foreign table testmulticorn 150 | -------------------------------------------------------------------------------- /test-3.9/expected/write_sqlalchemy.out: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | create or replace function create_foreign_server() returns void as $block$ 4 | DECLARE 5 | current_db varchar; 6 | BEGIN 7 | SELECT into current_db current_database(); 8 | EXECUTE $$ 9 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 10 | wrapper 'multicorn.sqlalchemyfdw.SqlAlchemyFdw', 11 | db_url 'postgresql://$$ || current_user || '@localhost/' || current_db || $$' 12 | ); 13 | $$; 14 | END; 15 | $block$ language plpgsql; 16 | select create_foreign_server(); 17 | create_foreign_server 18 | ----------------------- 19 | 20 | (1 row) 21 | 22 | create foreign table testalchemy ( 23 | id integer, 24 | adate date, 25 | atimestamp timestamp, 26 | anumeric numeric, 27 | avarchar varchar 28 | ) server multicorn_srv options ( 29 | tablename 'basetable' 30 | ); 31 | create table basetable ( 32 | id integer primary key, 33 | adate date, 34 | atimestamp timestamp, 35 | anumeric numeric, 36 | avarchar varchar 37 | ); 38 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 39 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'); 40 | NOTICE: You need to declare a primary key option in order to use the write features 41 | ERROR: This FDW does not support the writable API 42 | ALTER FOREIGN TABLE testalchemy OPTIONS (ADD primary_key 'id'); 43 | BEGIN; 44 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 45 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'), 46 | (2, '1990-03-05', '1998-03-02 10:40:18.321023', 12.2, 'Another Test'), 47 | (3, '1972-01-02', '1972-01-02 16:12:54', 4000, 'another Test'), 48 | (4, '1922-11-02', '1962-01-02 23:12:54', -3000, NULL); 49 | select * from basetable; 50 | id | adate | atimestamp | anumeric | avarchar 51 | ----+-------+------------+----------+---------- 52 | (0 rows) 53 | 54 | ROLLBACK; 55 | BEGIN; 56 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 57 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'), 58 | (2, '1990-03-05', '1998-03-02 10:40:18.321023', 12.2, 'Another Test'), 59 | (3, '1972-01-02', '1972-01-02 16:12:54', 4000, 'another Test'), 60 | (4, '1922-11-02', '1962-01-02 23:12:54', -3000, NULL); 61 | update testalchemy set avarchar = avarchar || ' UPDATED!'; 62 | COMMIT; 63 | SELECT * from basetable; 64 | id | adate | atimestamp | anumeric | avarchar 65 | ----+------------+--------------------------+----------+----------------------- 66 | 1 | 01-01-1980 | Tue Jan 01 11:01:21 1980 | 3.4 | Test UPDATED! 67 | 2 | 03-05-1990 | Mon Mar 02 10:40:18 1998 | 12.2 | Another Test UPDATED! 68 | 3 | 01-02-1972 | Sun Jan 02 16:12:54 1972 | 4000.0 | another Test UPDATED! 69 | 4 | 11-02-1922 | Tue Jan 02 23:12:54 1962 | -3000.0 | 70 | (4 rows) 71 | 72 | DELETE from testalchemy; 73 | SELECT * from basetable; 74 | id | adate | atimestamp | anumeric | avarchar 75 | ----+-------+------------+----------+---------- 76 | (0 rows) 77 | 78 | DROP EXTENSION multicorn cascade; 79 | NOTICE: drop cascades to 2 other objects 80 | DETAIL: drop cascades to server multicorn_srv 81 | drop cascades to foreign table testalchemy 82 | DROP TABLE basetable; 83 | -------------------------------------------------------------------------------- /test-3.9/expected/write_test.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying, 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | test_type 'nowrite', 12 | tx_hook 'true' 13 | ); 14 | insert into testmulticorn(test1, test2) VALUES ('test', 'test2'); 15 | NOTICE: [('option1', 'option1'), ('test_type', 'nowrite'), ('tx_hook', 'true'), ('usermapping', 'test')] 16 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 17 | NOTICE: BEGIN 18 | NOTICE: ROLLBACK 19 | ERROR: Error in python: NotImplementedError 20 | DETAIL: This FDW does not support the writable API 21 | update testmulticorn set test1 = 'test'; 22 | NOTICE: BEGIN 23 | NOTICE: [] 24 | NOTICE: ['test1', 'test2'] 25 | NOTICE: ROLLBACK 26 | ERROR: Error in python: NotImplementedError 27 | DETAIL: This FDW does not support the writable API 28 | delete from testmulticorn where test2 = 'test2 2 0'; 29 | NOTICE: BEGIN 30 | NOTICE: [test2 = test2 2 0] 31 | NOTICE: ['test1', 'test2'] 32 | NOTICE: ROLLBACK 33 | ERROR: Error in python: NotImplementedError 34 | DETAIL: This FDW does not support the writable API 35 | CREATE foreign table testmulticorn_write ( 36 | test1 character varying, 37 | test2 character varying 38 | ) server multicorn_srv options ( 39 | option1 'option1', 40 | row_id_column 'test1', 41 | test_type 'returning', 42 | tx_hook 'true' 43 | ); 44 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 45 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('tx_hook', 'true'), ('usermapping', 'test')] 46 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 47 | NOTICE: BEGIN 48 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 49 | NOTICE: PRECOMMIT 50 | NOTICE: COMMIT 51 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 52 | NOTICE: BEGIN 53 | NOTICE: [test1 ~~* test1 3%] 54 | NOTICE: ['test1', 'test2'] 55 | NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 56 | NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 57 | NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 58 | NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 59 | NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 60 | NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 61 | NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 62 | NOTICE: PRECOMMIT 63 | NOTICE: COMMIT 64 | delete from testmulticorn_write where test2 = 'test2 2 0'; 65 | NOTICE: BEGIN 66 | NOTICE: [test2 = test2 2 0] 67 | NOTICE: ['test1', 'test2'] 68 | NOTICE: DELETING: test1 1 0 69 | NOTICE: PRECOMMIT 70 | NOTICE: COMMIT 71 | -- Test returning 72 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2') RETURNING test1; 73 | NOTICE: BEGIN 74 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 75 | NOTICE: PRECOMMIT 76 | NOTICE: COMMIT 77 | test1 78 | ---------------- 79 | INSERTED: test 80 | (1 row) 81 | 82 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%' RETURNING test1; 83 | NOTICE: BEGIN 84 | NOTICE: [test1 ~~* test1 3%] 85 | NOTICE: ['test1', 'test2'] 86 | NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 87 | NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 88 | NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 89 | NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 90 | NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 91 | NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 92 | NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 93 | NOTICE: PRECOMMIT 94 | NOTICE: COMMIT 95 | test1 96 | --------------- 97 | UPDATED: test 98 | UPDATED: test 99 | UPDATED: test 100 | UPDATED: test 101 | UPDATED: test 102 | UPDATED: test 103 | UPDATED: test 104 | (7 rows) 105 | 106 | delete from testmulticorn_write where test1 = 'test1 1 0' returning test2, test1; 107 | NOTICE: BEGIN 108 | NOTICE: [test1 = test1 1 0] 109 | NOTICE: ['test1', 'test2'] 110 | NOTICE: DELETING: test1 1 0 111 | NOTICE: PRECOMMIT 112 | NOTICE: COMMIT 113 | test2 | test1 114 | -----------+----------- 115 | test2 2 0 | test1 1 0 116 | (1 row) 117 | 118 | DROP foreign table testmulticorn_write; 119 | -- Now test with another column 120 | CREATE foreign table testmulticorn_write( 121 | test1 character varying, 122 | test2 character varying 123 | ) server multicorn_srv options ( 124 | option1 'option1', 125 | row_id_column 'test2' 126 | ); 127 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 128 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('usermapping', 'test')] 129 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 130 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 131 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 132 | NOTICE: [test1 ~~* test1 3%] 133 | NOTICE: ['test1', 'test2'] 134 | NOTICE: UPDATING: test2 1 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 135 | NOTICE: UPDATING: test2 1 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 136 | NOTICE: UPDATING: test2 1 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 137 | NOTICE: UPDATING: test2 1 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 138 | NOTICE: UPDATING: test2 1 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 139 | NOTICE: UPDATING: test2 1 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 140 | NOTICE: UPDATING: test2 1 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 141 | delete from testmulticorn_write where test2 = 'test2 2 0'; 142 | NOTICE: [test2 = test2 2 0] 143 | NOTICE: ['test2'] 144 | NOTICE: DELETING: test2 2 0 145 | update testmulticorn_write set test2 = 'test' where test2 = 'test2 1 1'; 146 | NOTICE: [test2 = test2 1 1] 147 | NOTICE: ['test1', 'test2'] 148 | NOTICE: UPDATING: test2 1 1 with [('test1', 'test1 3 1'), ('test2', 'test')] 149 | DROP foreign table testmulticorn_write; 150 | -- Now test with other types 151 | CREATE foreign table testmulticorn_write( 152 | test1 date, 153 | test2 timestamp 154 | ) server multicorn_srv options ( 155 | option1 'option1', 156 | row_id_column 'test2', 157 | test_type 'date' 158 | ); 159 | insert into testmulticorn_write(test1, test2) VALUES ('2012-01-01', '2012-01-01 00:00:00'); 160 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('test_type', 'date'), ('usermapping', 'test')] 161 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 162 | NOTICE: INSERTING: [('test1', datetime.date(2012, 1, 1)), ('test2', datetime.datetime(2012, 1, 1, 0, 0))] 163 | delete from testmulticorn_write where test2 > '2011-12-03'; 164 | NOTICE: [test2 > 2011-12-03 00:00:00] 165 | NOTICE: ['test2'] 166 | NOTICE: DELETING: 2011-12-03 14:30:25 167 | update testmulticorn_write set test1 = date_trunc('day', test1) where test2 = '2011-09-03 14:30:25'; 168 | NOTICE: [test2 = 2011-09-03 14:30:25] 169 | NOTICE: ['test1', 'test2'] 170 | NOTICE: UPDATING: 2011-09-03 14:30:25 with [('test1', datetime.date(2011, 9, 2)), ('test2', datetime.datetime(2011, 9, 3, 14, 30, 25))] 171 | DROP foreign table testmulticorn_write; 172 | -- Test with unknown column 173 | CREATE foreign table testmulticorn_write( 174 | test1 date, 175 | test2 timestamp 176 | ) server multicorn_srv options ( 177 | option1 'option1', 178 | row_id_column 'teststuff', 179 | test_type 'date' 180 | ); 181 | delete from testmulticorn_write; 182 | NOTICE: [('option1', 'option1'), ('row_id_column', 'teststuff'), ('test_type', 'date'), ('usermapping', 'test')] 183 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 184 | ERROR: The rowid attribute does not exist 185 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 186 | DROP EXTENSION multicorn cascade; 187 | NOTICE: drop cascades to 3 other objects 188 | DETAIL: drop cascades to server multicorn_srv 189 | drop cascades to foreign table testmulticorn 190 | drop cascades to foreign table testmulticorn_write 191 | -------------------------------------------------------------------------------- /test-3.9/expected/write_test_1.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | CREATE foreign table testmulticorn ( 7 | test1 character varying, 8 | test2 character varying 9 | ) server multicorn_srv options ( 10 | option1 'option1', 11 | test_type 'nowrite', 12 | tx_hook 'true' 13 | ); 14 | insert into testmulticorn(test1, test2) VALUES ('test', 'test2'); 15 | NOTICE: [('option1', 'option1'), ('test_type', 'nowrite'), ('tx_hook', 'true'), ('usermapping', 'test')] 16 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 17 | NOTICE: BEGIN 18 | ERROR: Error in python: NotImplementedError 19 | DETAIL: This FDW does not support the writable API 20 | NOTICE: ROLLBACK 21 | update testmulticorn set test1 = 'test'; 22 | NOTICE: BEGIN 23 | NOTICE: [] 24 | NOTICE: ['test1', 'test2'] 25 | ERROR: Error in python: NotImplementedError 26 | DETAIL: This FDW does not support the writable API 27 | NOTICE: ROLLBACK 28 | delete from testmulticorn where test2 = 'test2 2 0'; 29 | NOTICE: BEGIN 30 | NOTICE: [test2 = test2 2 0] 31 | NOTICE: ['test1', 'test2'] 32 | ERROR: Error in python: NotImplementedError 33 | DETAIL: This FDW does not support the writable API 34 | NOTICE: ROLLBACK 35 | CREATE foreign table testmulticorn_write ( 36 | test1 character varying, 37 | test2 character varying 38 | ) server multicorn_srv options ( 39 | option1 'option1', 40 | row_id_column 'test1', 41 | test_type 'returning', 42 | tx_hook 'true' 43 | ); 44 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 45 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test1'), ('test_type', 'returning'), ('tx_hook', 'true'), ('usermapping', 'test')] 46 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 47 | NOTICE: BEGIN 48 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 49 | NOTICE: PRECOMMIT 50 | NOTICE: COMMIT 51 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 52 | NOTICE: BEGIN 53 | NOTICE: [test1 ~~* test1 3%] 54 | NOTICE: ['test1', 'test2'] 55 | NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 56 | NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 57 | NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 58 | NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 59 | NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 60 | NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 61 | NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 62 | NOTICE: PRECOMMIT 63 | NOTICE: COMMIT 64 | delete from testmulticorn_write where test2 = 'test2 2 0'; 65 | NOTICE: BEGIN 66 | NOTICE: [test2 = test2 2 0] 67 | NOTICE: ['test1', 'test2'] 68 | NOTICE: DELETING: test1 1 0 69 | NOTICE: PRECOMMIT 70 | NOTICE: COMMIT 71 | -- Test returning 72 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2') RETURNING test1; 73 | NOTICE: BEGIN 74 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 75 | NOTICE: PRECOMMIT 76 | NOTICE: COMMIT 77 | test1 78 | ---------------- 79 | INSERTED: test 80 | (1 row) 81 | 82 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%' RETURNING test1; 83 | NOTICE: BEGIN 84 | NOTICE: [test1 ~~* test1 3%] 85 | NOTICE: ['test1', 'test2'] 86 | NOTICE: UPDATING: test1 3 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 87 | NOTICE: UPDATING: test1 3 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 88 | NOTICE: UPDATING: test1 3 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 89 | NOTICE: UPDATING: test1 3 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 90 | NOTICE: UPDATING: test1 3 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 91 | NOTICE: UPDATING: test1 3 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 92 | NOTICE: UPDATING: test1 3 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 93 | NOTICE: PRECOMMIT 94 | NOTICE: COMMIT 95 | test1 96 | --------------- 97 | UPDATED: test 98 | UPDATED: test 99 | UPDATED: test 100 | UPDATED: test 101 | UPDATED: test 102 | UPDATED: test 103 | UPDATED: test 104 | (7 rows) 105 | 106 | delete from testmulticorn_write where test1 = 'test1 1 0' returning test2, test1; 107 | NOTICE: BEGIN 108 | NOTICE: [test1 = test1 1 0] 109 | NOTICE: ['test1', 'test2'] 110 | NOTICE: DELETING: test1 1 0 111 | NOTICE: PRECOMMIT 112 | NOTICE: COMMIT 113 | test2 | test1 114 | -----------+----------- 115 | test2 2 0 | test1 1 0 116 | (1 row) 117 | 118 | DROP foreign table testmulticorn_write; 119 | -- Now test with another column 120 | CREATE foreign table testmulticorn_write( 121 | test1 character varying, 122 | test2 character varying 123 | ) server multicorn_srv options ( 124 | option1 'option1', 125 | row_id_column 'test2' 126 | ); 127 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 128 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('usermapping', 'test')] 129 | NOTICE: [('test1', 'character varying'), ('test2', 'character varying')] 130 | NOTICE: INSERTING: [('test1', 'test'), ('test2', 'test2')] 131 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 132 | NOTICE: [test1 ~~* test1 3%] 133 | NOTICE: ['test1', 'test2'] 134 | NOTICE: UPDATING: test2 1 1 with [('test1', 'test'), ('test2', 'test2 1 1')] 135 | NOTICE: UPDATING: test2 1 4 with [('test1', 'test'), ('test2', 'test2 1 4')] 136 | NOTICE: UPDATING: test2 1 7 with [('test1', 'test'), ('test2', 'test2 1 7')] 137 | NOTICE: UPDATING: test2 1 10 with [('test1', 'test'), ('test2', 'test2 1 10')] 138 | NOTICE: UPDATING: test2 1 13 with [('test1', 'test'), ('test2', 'test2 1 13')] 139 | NOTICE: UPDATING: test2 1 16 with [('test1', 'test'), ('test2', 'test2 1 16')] 140 | NOTICE: UPDATING: test2 1 19 with [('test1', 'test'), ('test2', 'test2 1 19')] 141 | delete from testmulticorn_write where test2 = 'test2 2 0'; 142 | NOTICE: [test2 = test2 2 0] 143 | NOTICE: ['test2'] 144 | NOTICE: DELETING: test2 2 0 145 | update testmulticorn_write set test2 = 'test' where test2 = 'test2 1 1'; 146 | NOTICE: [test2 = test2 1 1] 147 | NOTICE: ['test1', 'test2'] 148 | NOTICE: UPDATING: test2 1 1 with [('test1', 'test1 3 1'), ('test2', 'test')] 149 | DROP foreign table testmulticorn_write; 150 | -- Now test with other types 151 | CREATE foreign table testmulticorn_write( 152 | test1 date, 153 | test2 timestamp 154 | ) server multicorn_srv options ( 155 | option1 'option1', 156 | row_id_column 'test2', 157 | test_type 'date' 158 | ); 159 | insert into testmulticorn_write(test1, test2) VALUES ('2012-01-01', '2012-01-01 00:00:00'); 160 | NOTICE: [('option1', 'option1'), ('row_id_column', 'test2'), ('test_type', 'date'), ('usermapping', 'test')] 161 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 162 | NOTICE: INSERTING: [('test1', datetime.date(2012, 1, 1)), ('test2', datetime.datetime(2012, 1, 1, 0, 0))] 163 | delete from testmulticorn_write where test2 > '2011-12-03'; 164 | NOTICE: [test2 > 2011-12-03 00:00:00] 165 | NOTICE: ['test2'] 166 | NOTICE: DELETING: 2011-12-03 14:30:25 167 | update testmulticorn_write set test1 = date_trunc('day', test1) where test2 = '2011-09-03 14:30:25'; 168 | NOTICE: [test2 = 2011-09-03 14:30:25] 169 | NOTICE: ['test1', 'test2'] 170 | NOTICE: UPDATING: 2011-09-03 14:30:25 with [('test1', datetime.date(2011, 9, 2)), ('test2', datetime.datetime(2011, 9, 3, 14, 30, 25))] 171 | DROP foreign table testmulticorn_write; 172 | -- Test with unknown column 173 | CREATE foreign table testmulticorn_write( 174 | test1 date, 175 | test2 timestamp 176 | ) server multicorn_srv options ( 177 | option1 'option1', 178 | row_id_column 'teststuff', 179 | test_type 'date' 180 | ); 181 | delete from testmulticorn_write; 182 | NOTICE: [('option1', 'option1'), ('row_id_column', 'teststuff'), ('test_type', 'date'), ('usermapping', 'test')] 183 | NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] 184 | ERROR: The rowid attribute does not exist 185 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 186 | DROP EXTENSION multicorn cascade; 187 | NOTICE: drop cascades to 3 other objects 188 | DETAIL: drop cascades to server multicorn_srv 189 | drop cascades to foreign table testmulticorn 190 | drop cascades to foreign table testmulticorn_write 191 | -------------------------------------------------------------------------------- /test-3.9/sql/disable_jit.include: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF current_setting('server_version_num')::bigint >= 110000 THEN 4 | SET jit = off; 5 | END IF; 6 | END; 7 | $$ LANGUAGE plpgsql; 8 | -------------------------------------------------------------------------------- /test-3.9/sql/import_sqlalchemy.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | create or replace function create_foreign_server() returns void as $block$ 4 | DECLARE 5 | current_db varchar; 6 | BEGIN 7 | SELECT into current_db current_database(); 8 | EXECUTE $$ 9 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 10 | wrapper 'multicorn.sqlalchemyfdw.SqlAlchemyFdw', 11 | db_url 'postgresql://$$ || current_user || '@localhost/' || current_db || $$' 12 | ); 13 | $$; 14 | END; 15 | $block$ language plpgsql; 16 | select create_foreign_server(); 17 | 18 | CREATE SCHEMA local_schema; 19 | CREATE TABLE local_schema.t1 ( 20 | c1 int primary key, 21 | c2 text, 22 | c3 timestamp, 23 | c4 numeric 24 | ); 25 | 26 | CREATE TABLE local_schema.t2 ( 27 | c1 int, 28 | c2 text, 29 | c3 timestamp, 30 | c4 numeric 31 | ); 32 | 33 | CREATE TABLE local_schema.t3 ( 34 | c1 int, 35 | c2 text, 36 | c3 timestamp, 37 | c4 numeric 38 | ); 39 | 40 | 41 | CREATE SCHEMA remote_schema; 42 | 43 | IMPORT FOREIGN SCHEMA local_schema FROM SERVER multicorn_srv INTO remote_schema ; 44 | SELECT * FROM remote_schema.t1; 45 | INSERT INTO remote_schema.t1 VALUES (1, '2', NULL, NULL); 46 | SELECT * FROM remote_schema.t1; 47 | DROP SCHEMA remote_schema CASCADE; 48 | CREATE SCHEMA remote_schema; 49 | IMPORT FOREIGN SCHEMA local_schema LIMIT TO (t1) FROM SERVER multicorn_srv INTO remote_schema ; 50 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'remote_schema'; 51 | IMPORT FOREIGN SCHEMA local_schema EXCEPT (t1, t3) FROM SERVER multicorn_srv INTO remote_schema ; 52 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'remote_schema'; 53 | DROP EXTENSION multicorn CASCADE; 54 | DROP SCHEMA local_schema CASCADE; 55 | DROP SCHEMA remote_schema CASCADE; 56 | -------------------------------------------------------------------------------- /test-3.9/sql/import_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE SCHEMA import_dest1; 7 | IMPORT FOREIGN SCHEMA import_source FROM SERVER multicorn_srv INTO import_dest1; 8 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1' ORDER BY relname; 9 | DROP SCHEMA import_dest1 CASCADE; 10 | CREATE SCHEMA import_dest1; 11 | IMPORT FOREIGN SCHEMA import_source EXCEPT (imported_table_1, imported_table_3) FROM SERVER multicorn_srv INTO import_dest1; 12 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1'; 13 | IMPORT FOREIGN SCHEMA import_source LIMIT TO (imported_table_1) FROM SERVER multicorn_srv INTO import_dest1; 14 | SELECT relname FROM pg_class c INNER JOIN pg_namespace n on c.relnamespace = n.oid WHERE n.nspname = 'import_dest1' ORDER BY relname; 15 | DROP EXTENSION multicorn cascade; 16 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_alchemy_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | 4 | create or replace function create_foreign_server() returns void as $block$ 5 | DECLARE 6 | current_db varchar; 7 | BEGIN 8 | SELECT into current_db current_database(); 9 | EXECUTE $$ 10 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 11 | wrapper 'multicorn.sqlalchemyfdw.SqlAlchemyFdw', 12 | db_url 'postgresql://$$ || current_user || '@localhost/' || current_db || $$' 13 | ); 14 | $$; 15 | END; 16 | $block$ language plpgsql; 17 | select create_foreign_server(); 18 | 19 | create foreign table testalchemy ( 20 | id integer, 21 | adate date, 22 | atimestamp timestamp, 23 | anumeric numeric, 24 | avarchar varchar 25 | ) server multicorn_srv options ( 26 | tablename 'basetable' 27 | ); 28 | 29 | create table basetable ( 30 | id integer, 31 | adate date, 32 | atimestamp timestamp, 33 | anumeric numeric, 34 | avarchar varchar 35 | ); 36 | 37 | insert into basetable (id, adate, atimestamp, anumeric, avarchar) values 38 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'), 39 | (2, '1990-03-05', '1998-03-02 10:40:18.321023', 12.2, 'Another Test'), 40 | (3, '1972-01-02', '1972-01-02 16:12:54', 4000, 'another Test'), 41 | (4, '1922-11-02', '1962-01-02 23:12:54', -3000, NULL); 42 | 43 | select * from testalchemy; 44 | 45 | select id, adate from testalchemy; 46 | 47 | select * from testalchemy where avarchar is null; 48 | 49 | select * from testalchemy where avarchar is not null; 50 | 51 | select * from testalchemy where adate > '1970-01-02'::date; 52 | 53 | select * from testalchemy where adate between '1970-01-01' and '1980-01-01'; 54 | 55 | select * from testalchemy where anumeric > 0; 56 | 57 | select * from testalchemy where avarchar not like '%test'; 58 | 59 | select * from testalchemy where avarchar like 'Another%'; 60 | 61 | select * from testalchemy where avarchar ilike 'Another%'; 62 | 63 | select * from testalchemy where avarchar not ilike 'Another%'; 64 | 65 | select * from testalchemy where id in (1,2); 66 | 67 | select * from testalchemy where id not in (1, 2); 68 | 69 | select * from testalchemy order by avarchar; 70 | 71 | select * from testalchemy order by avarchar desc; 72 | 73 | select * from testalchemy order by avarchar desc nulls first; 74 | 75 | select * from testalchemy order by avarchar desc nulls last; 76 | 77 | select * from testalchemy order by avarchar nulls first; 78 | 79 | select * from testalchemy order by avarchar nulls last; 80 | 81 | select count(*) from testalchemy; 82 | 83 | DROP EXTENSION multicorn cascade; 84 | DROP table basetable; 85 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_cache_invalidation.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying(20), 9 | test2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1' 12 | ); 13 | 14 | -- Test "normal" usage 15 | select * from testmulticorn; 16 | 17 | ALTER foreign table testmulticorn drop column test1; 18 | 19 | select * from testmulticorn; 20 | 21 | ALTER foreign table testmulticorn add column test1 varchar; 22 | 23 | select * from testmulticorn; 24 | 25 | ALTER foreign table testmulticorn add column test3 varchar; 26 | 27 | select * from testmulticorn; 28 | 29 | ALTER foreign table testmulticorn options (SET option1 'option1_update'); 30 | 31 | select * from testmulticorn; 32 | 33 | ALTER foreign table testmulticorn options (ADD option2 'option2'); 34 | 35 | select * from testmulticorn; 36 | 37 | ALTER foreign table testmulticorn options (DROP option2); 38 | 39 | select * from testmulticorn; 40 | 41 | -- Test dropping column when returning sequences (issue #15) 42 | ALTER foreign table testmulticorn options (ADD test_type 'sequence'); 43 | 44 | select * from testmulticorn; 45 | 46 | ALTER foreign table testmulticorn drop test3; 47 | 48 | select * from testmulticorn; 49 | 50 | ALTER foreign table testmulticorn alter test1 type varchar(30); 51 | 52 | select * from testmulticorn limit 1; 53 | 54 | ALTER foreign table testmulticorn alter test1 type text; 55 | 56 | select * from testmulticorn limit 1; 57 | 58 | ALTER foreign table testmulticorn rename test1 to testnew; 59 | 60 | select * from testmulticorn limit 1; 61 | 62 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 63 | DROP EXTENSION multicorn cascade; 64 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_column_options_test.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying options (prefix 'test'), 9 | test2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1' 12 | ); 13 | 14 | select * from testmulticorn limit 1; 15 | 16 | ALTER foreign table testmulticorn alter test1 options (set prefix 'test2'); 17 | 18 | select * from testmulticorn limit 1; 19 | 20 | ALTER foreign table testmulticorn alter test1 options (drop prefix); 21 | 22 | select * from testmulticorn limit 1; 23 | 24 | ALTER foreign table testmulticorn alter test1 options (add prefix 'test3'); 25 | 26 | select * from testmulticorn limit 1; 27 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 28 | DROP EXTENSION multicorn cascade; 29 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_error_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | -- Test that the wrapper option is required on the server. 4 | CREATE server multicorn_srv foreign data wrapper multicorn; 5 | -- Test that the wrapper option cannot be altered on the table 6 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 7 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 8 | ); 9 | CREATE foreign table testmulticorn ( 10 | test1 character varying, 11 | test2 character varying 12 | ) server multicorn_srv options ( 13 | option1 'option1', 14 | wrapper 'multicorn.evilwrapper.EvilDataWrapper' 15 | ); 16 | 17 | ALTER server multicorn_srv options (DROP wrapper); 18 | 19 | CREATE server multicorn_empty_srv foreign data wrapper multicorn; 20 | 21 | DROP EXTENSION multicorn cascade; 22 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_logger_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying, 9 | test2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'logger' 13 | ); 14 | 15 | -- Test "normal" usage 16 | select * from testmulticorn; 17 | 18 | DROP EXTENSION multicorn cascade; 19 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_planner_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | \i test-common/disable_jit.include 3 | CREATE EXTENSION multicorn; 4 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 5 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 6 | ); 7 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 8 | 9 | -- Test for two thing: first, that when a low total row count, 10 | -- a full seq scan is used on a join. 11 | CREATE foreign table testmulticorn ( 12 | test1 character varying, 13 | test2 character varying 14 | ) server multicorn_srv options ( 15 | option1 'option1' 16 | ); 17 | 18 | explain select * from testmulticorn; 19 | 20 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 21 | 22 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 23 | 24 | DROP foreign table testmulticorn; 25 | 26 | -- Second, when a total row count is high 27 | -- a parameterized path is used on the test1 attribute. 28 | CREATE foreign table testmulticorn ( 29 | test1 character varying, 30 | test2 character varying 31 | ) server multicorn_srv options ( 32 | option1 'option1', 33 | test_type 'planner', 34 | noisy_explain 'true' 35 | ); 36 | 37 | explain select * from testmulticorn; 38 | 39 | explain select * from testmulticorn m1 inner join testmulticorn m2 on m1.test1 = m2.test1; 40 | 41 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on m1.test1 = m2.test1; 42 | 43 | explain select * from testmulticorn m1 left outer join testmulticorn m2 on upper(m1.test1) = m2.test1; 44 | 45 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 46 | DROP EXTENSION multicorn cascade; 47 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_regression_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | 8 | CREATE foreign table testmulticorn ( 9 | test1 character varying, 10 | test2 character varying 11 | ) server multicorn_srv options ( 12 | option1 'option1' 13 | ); 14 | 15 | -- Test "normal" usage 16 | select * from testmulticorn; 17 | 18 | -- Test quals 19 | select * from testmulticorn where test1 like '%0'; 20 | select * from testmulticorn where test1 ilike '%0'; 21 | 22 | 23 | -- Test columns 24 | select test2 from testmulticorn; 25 | 26 | -- Test subquery plan 27 | select test1, (select max(substr(test1, 9, 1))::int as max from testmulticorn t2 where substr(t2.test1, 7, 1)::int = substr(t1.test1, 7, 1)::int) as max 28 | from testmulticorn t1 order by max desc; 29 | 30 | select test1, (select max(substr(test1, 9, 1))::int as max from testmulticorn t2 where t2.test1 = t1.test1) as max 31 | from testmulticorn t1 order by max desc; 32 | 33 | select * from testmulticorn where test1 is null; 34 | 35 | select * from testmulticorn where test1 is not null; 36 | 37 | select * from testmulticorn where 'grou' > test1; 38 | 39 | select * from testmulticorn where test1 < ANY(ARRAY['grou', 'MACHIN']); 40 | 41 | CREATE foreign table testmulticorn2 ( 42 | test1 character varying, 43 | test2 character varying 44 | ) server multicorn_srv options ( 45 | option1 'option2' 46 | ); 47 | 48 | select * from testmulticorn union all select * from testmulticorn2; 49 | 50 | create function test_function_immutable () returns varchar as $$ 51 | BEGIN 52 | RETURN 'test'; 53 | END 54 | $$ immutable language plpgsql; 55 | 56 | create function test_function_stable () returns varchar as $$ 57 | BEGIN 58 | RETURN 'test'; 59 | END 60 | $$ stable language plpgsql; 61 | 62 | create function test_function_volatile () returns varchar as $$ 63 | BEGIN 64 | RETURN 'test'; 65 | END 66 | $$ volatile language plpgsql; 67 | 68 | select * from testmulticorn where test1 like test_function_immutable(); 69 | 70 | select * from testmulticorn where test1 like test_function_stable(); 71 | 72 | select * from testmulticorn where test1 like test_function_volatile(); 73 | 74 | select * from testmulticorn where test1 like length(test2)::varchar; 75 | 76 | 77 | \set FETCH_COUNT 1000 78 | select * from testmulticorn; 79 | 80 | -- Test that zero values are converted to zero and not null 81 | ALTER FOREIGN TABLE testmulticorn options (add test_type 'int'); 82 | ALTER FOREIGN TABLE testmulticorn alter test1 type integer; 83 | 84 | select * from testmulticorn limit 1; 85 | 86 | select * from testmulticorn where test1 = 0; 87 | 88 | ALTER FOREIGN TABLE testmulticorn options (drop test_type); 89 | 90 | -- Test operations with bytea 91 | ALTER FOREIGN TABLE testmulticorn alter test2 type bytea; 92 | ALTER FOREIGN TABLE testmulticorn alter test1 type bytea; 93 | 94 | select encode(test1, 'escape') from testmulticorn where test2 = 'test2 1 19'::bytea; 95 | 96 | -- Test operations with None 97 | ALTER FOREIGN TABLE testmulticorn options (add test_type 'None'); 98 | 99 | select * from testmulticorn; 100 | 101 | ALTER FOREIGN TABLE testmulticorn options (set test_type 'iter_none'); 102 | 103 | select * from testmulticorn; 104 | 105 | ALTER FOREIGN TABLE testmulticorn add test3 money; 106 | 107 | SELECT * from testmulticorn where test3 = 12::money; 108 | SELECT * from testmulticorn where test1 = '12 €'; 109 | 110 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 111 | DROP EXTENSION multicorn cascade; 112 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_sequence_test.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | 8 | CREATE foreign table testmulticorn ( 9 | test1 character varying, 10 | test2 character varying 11 | ) server multicorn_srv options ( 12 | option1 'option1', 13 | test_type 'sequence' 14 | ); 15 | 16 | -- Test "normal" usage 17 | select * from testmulticorn; 18 | 19 | -- Test quals 20 | select * from testmulticorn where test1 like '%0'; 21 | select * from testmulticorn where test1 ilike '%0'; 22 | 23 | 24 | -- Test columns 25 | select test2 from testmulticorn; 26 | 27 | -- Test subquery plan 28 | select test1, (select max(substr(test1, 9, 1))::int as max from testmulticorn t2 where substr(t2.test1, 7, 1)::int = substr(t1.test1, 7, 1)::int) as max 29 | from testmulticorn t1 order by max desc; 30 | 31 | select test1, (select max(substr(test1, 9, 1))::int as max from testmulticorn t2 where t2.test1 = t1.test1) as max 32 | from testmulticorn t1 order by max desc; 33 | 34 | select * from testmulticorn where test1 is null; 35 | 36 | select * from testmulticorn where test1 is not null; 37 | 38 | select * from testmulticorn where 'grou' > test1; 39 | 40 | select * from testmulticorn where test1 < ANY(ARRAY['grou', 'MACHIN']); 41 | 42 | CREATE foreign table testmulticorn2 ( 43 | test1 character varying, 44 | test2 character varying 45 | ) server multicorn_srv options ( 46 | option1 'option2' 47 | ); 48 | 49 | select * from testmulticorn union all select * from testmulticorn2; 50 | 51 | CREATE foreign table testmulticorn3 ( 52 | test1 character varying, 53 | test2 character varying 54 | ) server multicorn_srv options ( 55 | option1 'option1', 56 | test_type 'sequence', 57 | test_subtype '1null' 58 | ); 59 | select * from testmulticorn3 limit 1; 60 | 61 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 62 | DROP EXTENSION multicorn cascade; 63 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_test_date.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | 8 | CREATE foreign table testmulticorn ( 9 | test1 date, 10 | test2 timestamp 11 | ) server multicorn_srv options ( 12 | option1 'option1', 13 | test_type 'date' 14 | ); 15 | 16 | -- Test "normal" usage 17 | select * from testmulticorn; 18 | 19 | select * from testmulticorn where test1 < '2011-06-01'; 20 | 21 | select * from testmulticorn where test2 < '2011-06-01 00:00:00'; 22 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 23 | DROP EXTENSION multicorn cascade; 24 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_test_dict.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE EXTENSION hstore; 4 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 5 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 6 | ); 7 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 8 | 9 | CREATE foreign table testmulticorn ( 10 | test1 hstore, 11 | test2 hstore 12 | ) server multicorn_srv options ( 13 | option1 'option1', 14 | test_type 'dict' 15 | ); 16 | 17 | -- Test "normal" usage 18 | select * from testmulticorn; 19 | 20 | select test1 -> 'repeater' as r from testmulticorn order by r; 21 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 22 | DROP EXTENSION multicorn cascade; 23 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_test_list.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | 8 | CREATE foreign table testmulticorn ( 9 | test1 character varying[], 10 | test2 character varying[] 11 | ) server multicorn_srv options ( 12 | option1 'option1', 13 | test_type 'list' 14 | ); 15 | 16 | -- Test "normal" usage 17 | select * from testmulticorn; 18 | 19 | select test1[2] as c from testmulticorn order by c; 20 | 21 | alter foreign table testmulticorn alter test1 type varchar; 22 | 23 | select * from testmulticorn; 24 | 25 | alter foreign table testmulticorn options (set test_type 'nested_list'); 26 | 27 | select * from testmulticorn limit 1; 28 | 29 | alter foreign table testmulticorn alter test1 type varchar[]; 30 | alter foreign table testmulticorn alter test2 type varchar[][]; 31 | 32 | select test1[2], test2[2][2], array_length(test1, 1), array_length(test2, 1), array_length(test2, 2) from testmulticorn limit 1; 33 | 34 | select length(test1[2]) from testmulticorn limit 1; 35 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 36 | DROP EXTENSION multicorn cascade; 37 | -------------------------------------------------------------------------------- /test-3.9/sql/multicorn_test_sort.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 4 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 5 | ); 6 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 7 | 8 | CREATE foreign table testmulticorn ( 9 | test1 date, 10 | test2 timestamp 11 | ) server multicorn_srv options ( 12 | option1 'option1', 13 | test_type 'date' 14 | ); 15 | 16 | -- Test sort pushdown asked 17 | EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 DESC; 18 | 19 | -- Data should be sorted 20 | SELECT * FROM testmulticorn ORDER BY test1 DESC; 21 | 22 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 23 | DROP EXTENSION multicorn cascade; 24 | -------------------------------------------------------------------------------- /test-3.9/sql/write_batch_test.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | 7 | CREATE foreign table testmulticorn_write( 8 | fname1 character varying, 9 | fname2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | row_id_column 'test1', 13 | modify_batch_size '2' 14 | ); 15 | 16 | insert into testmulticorn_write(fname1, fname2) 17 | VALUES 18 | ('col1_val1', 'col2_val'), 19 | ('col1_val2', 'col2_val'), 20 | ('col1_val3', 'col2_val'), 21 | ('col1_val4', 'col2_val'), 22 | ('col1_val5', 'col2_val'); 23 | 24 | CREATE OR REPLACE FUNCTION notify_insert() 25 | RETURNS TRIGGER AS $$ 26 | BEGIN 27 | RAISE NOTICE 'Inserted Record: %, %', NEW.fname1, NEW.fname2; 28 | RETURN NEW; 29 | END; 30 | $$ LANGUAGE plpgsql; 31 | 32 | CREATE TRIGGER after_insert_trigger 33 | AFTER INSERT ON testmulticorn_write 34 | FOR EACH ROW 35 | EXECUTE FUNCTION notify_insert(); 36 | 37 | INSERT INTO testmulticorn_write(fname1, fname2) 38 | VALUES 39 | ('col1_val6', 'col2_val'), 40 | ('col1_val7', 'col2_val'); 41 | 42 | DROP TRIGGER after_insert_trigger ON testmulticorn_write; 43 | DROP FUNCTION notify_insert(); 44 | 45 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 46 | DROP EXTENSION multicorn cascade; 47 | -------------------------------------------------------------------------------- /test-3.9/sql/write_filesystem.sql: -------------------------------------------------------------------------------- 1 | -- Setup the test 2 | CREATE EXTENSION multicorn; 3 | CREATE EXTENSION plpython3u; 4 | \i test-common/disable_jit.include 5 | 6 | CREATE OR REPLACE FUNCTION create_table() RETURNS VOID AS $$ 7 | import plpy 8 | import tempfile 9 | import os 10 | dir = tempfile.mkdtemp() 11 | plpy.execute(""" 12 | INSERT INTO temp_dir(dirname) VALUES ('%s') 13 | """ % str(dir)) 14 | plpy.execute(""" 15 | CREATE foreign table testmulticorn ( 16 | color varchar, 17 | size varchar, 18 | name varchar, 19 | ext varchar, 20 | filename varchar, 21 | data varchar 22 | ) server multicorn_srv options ( 23 | filename_column 'filename', 24 | content_column 'data', 25 | pattern '{color}/{size}/{name}.{ext}', 26 | root_dir '%s' 27 | ); 28 | """ % dir) 29 | for color in ('blue', 'red'): 30 | for size in ('big', 'small'): 31 | dirname = os.path.join(dir, color, size) 32 | os.makedirs(dirname) 33 | for name, ext in (('square', 'txt'), ('round', 'ini')): 34 | with open(os.path.join(dirname, '.'.join([name, ext])), 'a') as fd: 35 | fd.write('Im a %s %s %s\n' % (size, color, name)) 36 | $$ language plpython3u; 37 | 38 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 39 | wrapper 'multicorn.fsfdw.FilesystemFdw' 40 | ); 41 | 42 | 43 | CREATE TABLE temp_dir (dirname varchar); 44 | 45 | -- Create a table with the filesystem fdw in a temporary directory, 46 | -- and store the dirname in the temp_dir table. 47 | 48 | select create_table(); 49 | 50 | -- End of Setup 51 | 52 | \i test-common/multicorn_testfilesystem.include 53 | 54 | -- Cleanup everything we've done 55 | 56 | CREATE OR REPLACE FUNCTION cleanup_dir() RETURNS VOID AS $$ 57 | import shutil 58 | root_dir = plpy.execute("""SELECT dirname from temp_dir;""")[0]['dirname'] 59 | shutil.rmtree(root_dir) 60 | $$ language plpython3u; 61 | 62 | select cleanup_dir(); 63 | 64 | DROP FUNCTION cleanup_dir(); 65 | DROP TABLE temp_dir; 66 | DROP FUNCTION create_table(); 67 | DROP EXTENSION multicorn cascade; 68 | DROP EXTENSION plpython3u; 69 | -------------------------------------------------------------------------------- /test-3.9/sql/write_savepoints.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying, 9 | test2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'nowrite' 13 | ); 14 | 15 | -- No savepoints 16 | BEGIN; 17 | 18 | CREATE foreign table testmulticorn_write ( 19 | test1 character varying, 20 | test2 character varying 21 | ) server multicorn_srv options ( 22 | option1 'option1', 23 | row_id_column 'test1', 24 | test_type 'returning' 25 | ); 26 | 27 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 28 | 29 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 30 | 31 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 32 | 33 | delete from testmulticorn_write where test1 = '1'; 34 | 35 | DROP foreign table testmulticorn_write; 36 | 37 | ROLLBACK; 38 | 39 | -- One savepoint 40 | BEGIN; 41 | 42 | CREATE foreign table testmulticorn_write ( 43 | test1 character varying, 44 | test2 character varying 45 | ) server multicorn_srv options ( 46 | option1 'option1', 47 | row_id_column 'test1', 48 | test_type 'returning' 49 | ); 50 | 51 | SAVEPOINT A; 52 | 53 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 54 | 55 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 56 | 57 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 58 | 59 | delete from testmulticorn_write where test1 = '1'; 60 | 61 | ROLLBACK TO A; 62 | 63 | RELEASE A; 64 | 65 | DROP foreign table testmulticorn_write; 66 | 67 | COMMIT; 68 | 69 | -- Multiple sequential savepoints 70 | BEGIN; 71 | 72 | CREATE foreign table testmulticorn_write ( 73 | test1 character varying, 74 | test2 character varying 75 | ) server multicorn_srv options ( 76 | option1 'option1', 77 | row_id_column 'test1', 78 | test_type 'returning' 79 | ); 80 | 81 | SAVEPOINT A; 82 | 83 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 84 | 85 | select * from testmulticorn LIMIT 1; 86 | 87 | ROLLBACK TO A; 88 | 89 | RELEASE A; 90 | 91 | SAVEPOINT B; 92 | 93 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 94 | 95 | RELEASE B; 96 | 97 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 98 | 99 | delete from testmulticorn_write where test1 = '1'; 100 | 101 | DROP foreign table testmulticorn_write; 102 | 103 | ROLLBACK; 104 | 105 | -- Multiple nested savepoints 106 | BEGIN; 107 | 108 | CREATE foreign table testmulticorn_write ( 109 | test1 character varying, 110 | test2 character varying 111 | ) server multicorn_srv options ( 112 | option1 'option1', 113 | row_id_column 'test1', 114 | test_type 'returning' 115 | ); 116 | 117 | SAVEPOINT A; 118 | 119 | insert into testmulticorn_write(test1, test2) VALUES ('0', 'A'); 120 | 121 | select * from testmulticorn LIMIT 1; 122 | 123 | SAVEPOINT B; 124 | 125 | update testmulticorn_write set test2 = 'B' where test1 = '0'; 126 | 127 | RELEASE B; 128 | 129 | update testmulticorn_write set test2 = 'C' where test1 = '1'; 130 | 131 | delete from testmulticorn_write where test1 = '1'; 132 | 133 | ROLLBACK TO A; 134 | 135 | RELEASE A; 136 | 137 | DROP foreign table testmulticorn_write; 138 | 139 | ROLLBACK; 140 | 141 | -- Clean up 142 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 143 | DROP EXTENSION multicorn cascade; 144 | -------------------------------------------------------------------------------- /test-3.9/sql/write_sqlalchemy.sql: -------------------------------------------------------------------------------- 1 | SET client_min_messages=NOTICE; 2 | CREATE EXTENSION multicorn; 3 | 4 | create or replace function create_foreign_server() returns void as $block$ 5 | DECLARE 6 | current_db varchar; 7 | BEGIN 8 | SELECT into current_db current_database(); 9 | EXECUTE $$ 10 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 11 | wrapper 'multicorn.sqlalchemyfdw.SqlAlchemyFdw', 12 | db_url 'postgresql://$$ || current_user || '@localhost/' || current_db || $$' 13 | ); 14 | $$; 15 | END; 16 | $block$ language plpgsql; 17 | select create_foreign_server(); 18 | 19 | create foreign table testalchemy ( 20 | id integer, 21 | adate date, 22 | atimestamp timestamp, 23 | anumeric numeric, 24 | avarchar varchar 25 | ) server multicorn_srv options ( 26 | tablename 'basetable' 27 | ); 28 | 29 | create table basetable ( 30 | id integer primary key, 31 | adate date, 32 | atimestamp timestamp, 33 | anumeric numeric, 34 | avarchar varchar 35 | ); 36 | 37 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 38 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'); 39 | 40 | ALTER FOREIGN TABLE testalchemy OPTIONS (ADD primary_key 'id'); 41 | 42 | 43 | BEGIN; 44 | 45 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 46 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'), 47 | (2, '1990-03-05', '1998-03-02 10:40:18.321023', 12.2, 'Another Test'), 48 | (3, '1972-01-02', '1972-01-02 16:12:54', 4000, 'another Test'), 49 | (4, '1922-11-02', '1962-01-02 23:12:54', -3000, NULL); 50 | 51 | select * from basetable; 52 | 53 | ROLLBACK; 54 | 55 | BEGIN; 56 | 57 | 58 | insert into testalchemy (id, adate, atimestamp, anumeric, avarchar) values 59 | (1, '1980-01-01', '1980-01-01 11:01:21.132912', 3.4, 'Test'), 60 | (2, '1990-03-05', '1998-03-02 10:40:18.321023', 12.2, 'Another Test'), 61 | (3, '1972-01-02', '1972-01-02 16:12:54', 4000, 'another Test'), 62 | (4, '1922-11-02', '1962-01-02 23:12:54', -3000, NULL); 63 | 64 | update testalchemy set avarchar = avarchar || ' UPDATED!'; 65 | 66 | COMMIT; 67 | 68 | SELECT * from basetable; 69 | 70 | DELETE from testalchemy; 71 | 72 | SELECT * from basetable; 73 | 74 | DROP EXTENSION multicorn cascade; 75 | DROP TABLE basetable; 76 | -------------------------------------------------------------------------------- /test-3.9/sql/write_test.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION multicorn; 2 | CREATE server multicorn_srv foreign data wrapper multicorn options ( 3 | wrapper 'multicorn.testfdw.TestForeignDataWrapper' 4 | ); 5 | CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); 6 | 7 | CREATE foreign table testmulticorn ( 8 | test1 character varying, 9 | test2 character varying 10 | ) server multicorn_srv options ( 11 | option1 'option1', 12 | test_type 'nowrite', 13 | tx_hook 'true' 14 | ); 15 | 16 | insert into testmulticorn(test1, test2) VALUES ('test', 'test2'); 17 | 18 | update testmulticorn set test1 = 'test'; 19 | 20 | delete from testmulticorn where test2 = 'test2 2 0'; 21 | 22 | CREATE foreign table testmulticorn_write ( 23 | test1 character varying, 24 | test2 character varying 25 | ) server multicorn_srv options ( 26 | option1 'option1', 27 | row_id_column 'test1', 28 | test_type 'returning', 29 | tx_hook 'true' 30 | ); 31 | 32 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 33 | 34 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 35 | 36 | delete from testmulticorn_write where test2 = 'test2 2 0'; 37 | 38 | -- Test returning 39 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2') RETURNING test1; 40 | 41 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%' RETURNING test1; 42 | 43 | delete from testmulticorn_write where test1 = 'test1 1 0' returning test2, test1; 44 | 45 | DROP foreign table testmulticorn_write; 46 | -- Now test with another column 47 | CREATE foreign table testmulticorn_write( 48 | test1 character varying, 49 | test2 character varying 50 | ) server multicorn_srv options ( 51 | option1 'option1', 52 | row_id_column 'test2' 53 | ); 54 | 55 | insert into testmulticorn_write(test1, test2) VALUES ('test', 'test2'); 56 | 57 | update testmulticorn_write set test1 = 'test' where test1 ilike 'test1 3%'; 58 | 59 | delete from testmulticorn_write where test2 = 'test2 2 0'; 60 | 61 | update testmulticorn_write set test2 = 'test' where test2 = 'test2 1 1'; 62 | 63 | DROP foreign table testmulticorn_write; 64 | -- Now test with other types 65 | CREATE foreign table testmulticorn_write( 66 | test1 date, 67 | test2 timestamp 68 | ) server multicorn_srv options ( 69 | option1 'option1', 70 | row_id_column 'test2', 71 | test_type 'date' 72 | ); 73 | 74 | insert into testmulticorn_write(test1, test2) VALUES ('2012-01-01', '2012-01-01 00:00:00'); 75 | 76 | delete from testmulticorn_write where test2 > '2011-12-03'; 77 | 78 | update testmulticorn_write set test1 = date_trunc('day', test1) where test2 = '2011-09-03 14:30:25'; 79 | 80 | DROP foreign table testmulticorn_write; 81 | -- Test with unknown column 82 | CREATE foreign table testmulticorn_write( 83 | test1 date, 84 | test2 timestamp 85 | ) server multicorn_srv options ( 86 | option1 'option1', 87 | row_id_column 'teststuff', 88 | test_type 'date' 89 | ); 90 | delete from testmulticorn_write; 91 | DROP USER MAPPING FOR current_user SERVER multicorn_srv; 92 | DROP EXTENSION multicorn cascade; 93 | -------------------------------------------------------------------------------- /test-common/disable_jit.include: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF current_setting('server_version_num')::bigint >= 110000 THEN 4 | SET jit = off; 5 | END IF; 6 | END; 7 | $$ LANGUAGE plpgsql; 8 | --------------------------------------------------------------------------------