pax_global_header00006660000000000000000000000064146373335720014527gustar00rootroot0000000000000052 comment=8d5f979a2540c1254dda9d72f70c774c55e7c73f pgsql-ogr-fdw-1.1.5/000077500000000000000000000000001463733357200142245ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/.astylerc000066400000000000000000000003451463733357200160550ustar00rootroot00000000000000--style=allman --indent=tab=4 --max-code-length=120 --lineend=linux --unpad-paren --pad-oper --pad-header --align-pointer=type --align-reference=type --break-closing-braces --add-braces --break-return-type --break-after-logical pgsql-ogr-fdw-1.1.5/.editorconfig000066400000000000000000000006731463733357200167070ustar00rootroot00000000000000# http://editorconfig.org # top-most EditorConfig file root = true # these are the defaults [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true # Test files need kid gloves [*.{source,out}] insert_final_newline = true trim_trailing_whitespace = false # C files want tab indentation [*.{c,h,md}] indent_style = tab indent_size = 4 # Makefiles want tab indentation [Makefile] indent_style = tab pgsql-ogr-fdw-1.1.5/.github/000077500000000000000000000000001463733357200155645ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/.github/workflows/000077500000000000000000000000001463733357200176215ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/.github/workflows/ci.yml000066400000000000000000000042141463733357200207400ustar00rootroot00000000000000# GitHub Actions for OGR-FDW # # Paul Ramsey name: "CI" on: push: # branches-ignore: # - 'master' pull_request: ~ jobs: linux: name: "CI" strategy: fail-fast: false matrix: ci: - { PGVER: 13 } - { PGVER: 14 } - { PGVER: 15 } - { PGVER: 16 } - { PGVER: 17 } runs-on: ubuntu-latest steps: - name: 'Check Out' uses: actions/checkout@v4 - name: 'Raise Priority for apt.postgresql.org' run: | cat << EOF >> ./pgdg.pref Package: * Pin: release o=apt.postgresql.org Pin-Priority: 600 EOF sudo mv ./pgdg.pref /etc/apt/preferences.d/ sudo apt update - name: 'Install GDAL' run: | sudo apt-get install libgdal-dev - name: 'Install PostgreSQL' run: | sudo apt-get purge postgresql-* sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg-snapshot main ${{ matrix.ci.PGVER }}" > /etc/apt/sources.list.d/pgdg.list' curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg >/dev/null sudo apt-get update sudo apt-get -y install postgresql-${{ matrix.ci.PGVER }} postgresql-server-dev-${{ matrix.ci.PGVER }} - name: 'Start PostgreSQL' run: | export PGVER=${{ matrix.ci.PGVER }} export PGDATA=/var/lib/postgresql/$PGVER/main export PGETC=/etc/postgresql/$PGVER/main export PGBIN=/usr/lib/postgresql/$PGVER/bin export RUNNER_USER=`whoami` # make sure postgres user can access data files sudo chmod -R 755 /home/${RUNNER_USER} sudo cp ./ci/pg_hba.conf $PGETC/pg_hba.conf sudo systemctl stop postgresql sudo pg_ctlcluster $PGVER main start sudo pg_lsclusters - name: 'Build & Test' run: | export PATH=/usr/lib/postgresql/${{ matrix.ci.PGVER }}/bin/:$PATH PG_CFLAGS=-Werror make sudo make install PGUSER=postgres make installcheck || (cat regression.diffs && /bin/false) pgsql-ogr-fdw-1.1.5/.gitignore000066400000000000000000000002371463733357200162160ustar00rootroot00000000000000*.o *.a *.so *.dll *.dylib *.pc ogr_fdw_info.dSYM/ ogr_fdw_info* sql/ogr_fdw.sql expected/ogr_fdw.out results/ regression.diffs regression.out tmp_check/ log/ pgsql-ogr-fdw-1.1.5/FAQ.md000066400000000000000000000040321463733357200151540ustar00rootroot00000000000000# Frequently Asked Questions ### ODBC connections work in `ogr_fdw_info.exe` but not in the database, why? 1. One possibility is that you registered DSN under USER instead of System. It should be system since otherwise it will only work under the account you are logged in as. Since `ogr_fdw_info.exe` is a client app, it will give you a false sense of success since ODBC will in server, run under the context of the PostgreSQL service account. Verify that you have a System DSN (and **not** a User DSN). 2. If you used the Windows installer for PostgreSQL, it starts up PostgreSQL using Network Service account. I always manually switch it to a real user account since I need it to access some network resources. I never tested, but I suspect ODBC keys may not be readable by Network Service account. If change #1 doesn't work, try changing PostgreSQL to run under a regular user account. Make sure that user has full control of the PostgreSQL data folder. ### Do I need ODBC to read an MS Access database? You shouldn't need to do ODBC for MS Access, should be able to do: ogr_fdw_info.exe -s "D:\FDW Data\My Folder\MYFILE.MDB" As an added bonus, using a direct access shouldn't need to read ODBC registry keys since it's a DNSless connection. ### Why doesn't my MS Access connection work? If your MS Access database is on a network share, your PostgreSQL service account needs to be able to access it by the path you use. For MS Access databases it also has to have write permissions. The reason is MS Access databases use a locking file to manage access, so all users that have read permission also need to have write permission into the folder to create the lock file and delete the lock file (if they are the last ones in) if it doesn't exist. Also note: even if the PostgreSQL service account can access the folder `\\S\Files`, if you map it to say `S:\` the connection will not work if the mapped drive is not set under the user account that PostgreSQL runs under. (That said – it's safer to use UNCs (`\\S\Files`) instead of mapped drives. pgsql-ogr-fdw-1.1.5/LICENSE.md000066400000000000000000000020721463733357200156310ustar00rootroot00000000000000Copyright (C) 2014 Paul Ramsey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.pgsql-ogr-fdw-1.1.5/META.json000066400000000000000000000023671463733357200156550ustar00rootroot00000000000000{ "name": "ogr_fdw", "abstract": "OGR foreign data wrapper", "description": "OGR FDW allows you to connect to any OGR supported data source.", "version": "1.1.0", "maintainer": [ "Paul Ramsey " ], "license": { "mit": "http://en.wikipedia.org/wiki/MIT_License" }, "prereqs": { "runtime": { "requires": { "PostgreSQL": "9.3.0" }, "recommends": { "PostgreSQL": "9.3.4" } } }, "provides": { "ogr_fdw": { "file": "ogr_fdw--1.1.sql", "docfile": "README.md", "version": "1.1.0", "abstract": "OGR FDW wrapper" } }, "resources": { "homepage": "https://github.com/pramsey/pgsql-ogr-fdw/", "bugtracker": { "web": "https://github.com/pramsey/pgsql-ogr-fdw/issues" }, "repository": { "url": "https://github.com/pramsey/pgsql-ogr-fdw.git", "web": "https://github.com/pramsey/pgsql-ogr-fdw/", "type": "git" } }, "generated_by": "Paul Ramsey", "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" }, "tags": [ "ogr", "gdal", "gis", "fdw", "postgis" ] } pgsql-ogr-fdw-1.1.5/Makefile000066400000000000000000000040241463733357200156640ustar00rootroot00000000000000# ogr_fdw/Makefile MODULE_big = ogr_fdw OBJS = \ ogr_fdw.o \ ogr_fdw_deparse.o \ ogr_fdw_common.o \ ogr_fdw_func.o \ stringbuffer_pg.o EXTENSION = ogr_fdw DATA = \ ogr_fdw--1.0--1.1.sql \ ogr_fdw--1.1.sql REGRESS = ogr_fdw EXTRA_CLEAN = sql/*.sql expected/*.out GDAL_CONFIG = gdal-config GDAL_CFLAGS = $(shell $(GDAL_CONFIG) --cflags) GDAL_LIBS = $(shell $(GDAL_CONFIG) --libs) PG_CONFIG = pg_config REGRESS_OPTS = --encoding=UTF8 PG_CPPFLAGS += $(GDAL_CFLAGS) LIBS += $(GDAL_LIBS) SHLIB_LINK := $(LIBS) # For MacOS #SHLIB_LINK += -rpath /usr/local/lib PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) PG_VERSION_NUM = $(shell pg_config --version | cut -f2 -d' ' | awk -F. '{printf "%d%04d", $$1, $$2}') HAS_IMPORT_SCHEMA = $(shell [ $(PG_VERSION_NUM) -ge 90500 ] && echo yes) # order matters, file first, import last REGRESS = file pgsql ifeq ($(HAS_IMPORT_SCHEMA),yes) REGRESS += import endif # work around pg15 change to regression file variable # substitution for @abs_srcdir@ until we can drop older # version support # https://github.com/postgres/postgres/commit/d1029bb5a26cb84b116b0dee4dde312291359f2a PG15 := $(shell [ $(PG_VERSION_NUM) -ge 150000 ] && echo yes) ifeq ($(PG15),yes) sql/%.sql: input/%.source perl -pe 's#\@abs_srcdir\@#$(PWD)#g' < $< > $@ expected/%.out: output/%.source perl -pe 's#\@abs_srcdir\@#$(PWD)#g' < $< > $@ SQLFILES := sql/file.sql sql/import.sql sql/pgsql.sql sql/postgis.sql OUTFILES := expected/file.out expected/import.out expected/pgsql.out expected/postgis.out installcheck: $(SQLFILES) $(OUTFILES) endif ############################################################### # Build the utility program after PGXS to override the # PGXS environment CFLAGS = $(GDAL_CFLAGS) LIBS = $(GDAL_LIBS) ogr_fdw_info$(X): ogr_fdw_info.o ogr_fdw_common.o stringbuffer.o $(CC) $(CFLAGS) -o $@ $^ $(LIBS) clean-exe: rm -f ogr_fdw_info$(X) ogr_fdw_info.o stringbuffer.o install-exe: all $(INSTALL_PROGRAM) ogr_fdw_info$(X) '$(DESTDIR)$(bindir)' all: ogr_fdw_info$(X) clean: clean-exe install: install-exe pgsql-ogr-fdw-1.1.5/README.md000066400000000000000000000401001463733357200154760ustar00rootroot00000000000000[![Build Status](https://github.com/pramsey/pgsql-ogr-fdw/actions/workflows/ci.yml/badge.svg)](https://github.com/pramsey/pgsql-ogr-fdw/actions/workflows/ci.yml) # PostgreSQL OGR Foreign Data Wrapper ## Motivation OGR is the **vector** half of the [GDAL](http://www.gdal.org/) spatial data access library. It allows access to a [large number of GIS data formats](http://www.gdal.org/ogr_formats.html) using a [simple C API](http://www.gdal.org/ogr__api_8h.html) for data reading and writing. Since OGR exposes a simple table structure and PostgreSQL [foreign data wrappers](https://wiki.postgresql.org/wiki/Foreign_data_wrappers) allow access to table structures, the fit seems pretty perfect. ## Limitations This implementation currently has the following limitations: * **PostgreSQL 9.3 or higher.** This wrapper does not support the FDW implementations in older versions of PostgreSQL. * **Limited non-spatial query restrictions are pushed down to OGR.** OGR only supports a [minimal set](https://gdal.org/user/ogr_sql_dialect.html) of SQL operators (>, <, <=, >=, =). * **Only bounding box filters (&&) are pushed down.** Spatial filtering is possible, but only bounding boxes, and only using the && operator. * **OGR connections every time** Rather than pooling OGR connections, each query makes (and disposes of) **two** new ones, which seems to be the largest performance drag at the moment for restricted (small) queries. * **All columns are retrieved every time.** PostgreSQL foreign data wrappers don't require all columns all the time, and some efficiencies can be gained by only requesting the columns needed to fulfill a query. This would be a minimal efficiency improvement, but can be removed given some development time, since the OGR API supports returning a subset of columns. ## Download * Windows * Via [Stackbuilder](https://www.postgresql.org/download/windows/) (part of PostGIS Bundle) * Linux * [Arch Linux](https://aur.archlinux.org/packages/pgsql-ogr-fdw/) * [Ubuntu](https://launchpad.net/ubuntu/+source/pgsql-ogr-fdw), [PGDG Apt: Debian / Ubuntu](https://apt.postgresql.org) * [PGDG Yum: Redhat EL, CentOS, Rocky](https://yum.postgresql.org/) * OSX ## Basic Operation In order to access geometry data from OGR, the PostGIS extension has to be installed: if it is not installed, geometry will be represented as bytea columns, with well-known binary (WKB) values. To build the wrapper, make sure you have the GDAL library and development packages (is `gdal-config` on your path?) installed, as well as the PostgreSQL development packages (is `pg_config` on your path?) Build the wrapper with `make` and `make install`. Now you are ready to create a foreign table. First install the `postgis` and `ogr_fdw` extensions in your database. ```sql -- Install the required extensions CREATE EXTENSION postgis; CREATE EXTENSION ogr_fdw; ``` For a test data set, copy the `pt_two` example shape file from the `data` directory to a location where the PostgreSQL server can read it (like `/tmp/test/` for example). Use the `ogr_fdw_info` tool to read an OGR data source and output a server and table definition for a particular layer. (You can write these manually, but the utility makes it a little more foolproof.) ``` # ogr_fdw_info -f Supported Formats: -> "PCIDSK" (read/write) -> "netCDF" (read/write) ... -> "HTTP" (readonly) # ogr_fdw_info -s /tmp/test Layers: pt_two # ogr_fdw_info -s /tmp/test -l pt_two CREATE SERVER "myserver" FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/tmp/test', format 'ESRI Shapefile' ); CREATE FOREIGN TABLE "pt_two" ( fid integer, "geom" geometry(Point, 4326), "name" varchar, "age" integer, "height" real, "birthdate" date ) SERVER "myserver" OPTIONS (layer 'pt_two'); ``` Copy the `CREATE SERVER` and `CREATE FOREIGN SERVER` SQL commands into the database and you'll have your foreign table definition. ``` Foreign table "public.pt_two" Column | Type | Modifiers | FDW Options ----------+-------------------+-----------+------------- fid | integer | | geom | geometry | | name | character varying | | age | integer | | height | real | | birthday | date | | Server: tmp_shape FDW Options: (layer 'pt_two') ``` And you can query the table directly, even though it's really just a shape file. ```sql SELECT * FROM pt_two; ``` ``` fid | geom | name | age | height | birthday -----+--------------------------------------------+-------+-----+--------+------------ 0 | 0101000000C00497D1162CB93F8CBAEF08A080E63F | Peter | 45 | 5.6 | 1965-04-12 1 | 010100000054E943ACD697E2BFC0895EE54A46CF3F | Paul | 33 | 5.84 | 1971-03-25 ``` You can also apply filters, and see the portions that will be pushed down to the OGR driver, by setting the debug level to `DEBUG1`. ```sql SET client_min_messages = debug1; SELECT name, age, height FROM pt_two WHERE height < 5.7 AND geom && ST_MakeEnvelope(0, 0, 1, 1); ``` ``` DEBUG: OGR SQL: (height < 5.7) DEBUG: OGR spatial filter (0 0, 1 1) name | age | height -------+-----+-------- Peter | 45 | 5.6 (1 row) ``` ## Examples ### WFS FDW Since we can access any OGR data source as a table, how about a public WFS server? ```sql CREATE EXTENSION postgis; CREATE EXTENSION ogr_fdw; CREATE SERVER geoserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'WFS:https://demo.geo-solutions.it/geoserver/wfs', format 'WFS' ); CREATE FOREIGN TABLE topp_states ( fid bigint, the_geom Geometry(MultiSurface,4326), gml_id varchar, state_name varchar, state_fips varchar, sub_region varchar, state_abbr varchar, land_km double precision, water_km double precision, persons double precision, families double precision, houshold double precision, male double precision, female double precision, workers double precision, drvalone double precision, carpool double precision, pubtrans double precision, employed double precision, unemploy double precision, service double precision, manual double precision, p_male double precision, p_female double precision, samp_pop double precision ) SERVER "geoserver" OPTIONS (layer 'topp:states'); ``` ### FGDB FDW Unzip the `Querying.zip` file from the `data` directory to get a `Querying.gdb` file, and put it somewhere public (like `/tmp`). Now run the `ogr_fdw_info` tool on it to get a table definition. ```sql CREATE SERVER fgdbtest FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/tmp/Querying.gdb', format 'OpenFileGDB' ); CREATE FOREIGN TABLE cities ( fid integer, geom geometry(Point, 4326), city_fips varchar, city_name varchar, state_fips varchar, state_name varchar, state_city varchar, type varchar, capital varchar, elevation integer, pop1990 integer, popcat integer ) SERVER fgdbtest OPTIONS (layer 'Cities'); ``` Query away! ### PostgreSQL FDW Wraparound action! Handy for testing. Connect your database back to your database and watch the fur fly. This is only for testing, for best performance you should use postgres_fdw foreign data wrapper even when querying a PostGIS enabled database. ```sql CREATE TABLE apostles ( fid serial primary key, geom geometry(Point, 4326), joined integer, name text, height real, born date, clock time, ts timestamp ); INSERT INTO apostles (name, geom, joined, height, born, clock, ts) VALUES ('Peter', 'SRID=4326;POINT(30.31 59.93)', 1, 1.6, '1912-01-10', '10:10:01', '1912-01-10 10:10:01'), ('Andrew', 'SRID=4326;POINT(-2.8 56.34)', 2, 1.8, '1911-02-11', '10:10:02', '1911-02-11 10:10:02'), ('James', 'SRID=4326;POINT(-79.23 42.1)', 3, 1.72, '1910-03-12', '10:10:03', '1910-03-12 10:10:03'), ('John', 'SRID=4326;POINT(13.2 47.35)', 4, 1.45, '1909-04-01', '10:10:04', '1909-04-01 10:10:04'), ('Philip', 'SRID=4326;POINT(-75.19 40.69)', 5, 1.65, '1908-05-02', '10:10:05', '1908-05-02 10:10:05'), ('Bartholomew', 'SRID=4326;POINT(-62 18)', 6, 1.69, '1907-06-03', '10:10:06', '1907-06-03 10:10:06'), ('Thomas', 'SRID=4326;POINT(-80.08 35.88)', 7, 1.68, '1906-07-04', '10:10:07', '1906-07-04 10:10:07'), ('Matthew', 'SRID=4326;POINT(-73.67 20.94)', 8, 1.65, '1905-08-05', '10:10:08', '1905-08-05 10:10:08'), ('James Alpheus', 'SRID=4326;POINT(-84.29 34.07)', 9, 1.78, '1904-09-06', '10:10:09', '1904-09-06 10:10:09'), ('Thaddaeus', 'SRID=4326;POINT(79.13 10.78)', 10, 1.88, '1903-10-07', '10:10:10', '1903-10-07 10:10:10'), ('Simon', 'SRID=4326;POINT(-85.97 41.75)', 11, 1.61, '1902-11-08', '10:10:11', '1902-11-08 10:10:11'), ('Judas Iscariot', 'SRID=4326;POINT(35.7 32.4)', 12, 1.71, '1901-12-09', '10:10:12', '1901-12-09 10:10:12'); CREATE SERVER wraparound FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'Pg:dbname=fdw user=postgres', format 'PostgreSQL' ); CREATE FOREIGN TABLE apostles_fdw ( fid integer, geom geometry(Point, 4326), joined integer, name text, height real, born date, clock time, ts timestamp ) SERVER wraparound OPTIONS (layer 'apostles'); SELECT * FROM apostles_fdw; ``` ## Advanced Features ### Writeable FDW Tables If the OGR driver you are using supports it, you can insert/update/delete records from your FDW tables. For file-backed drivers, the user under which `postgres` runs will need read/write access to the file being altered. For database-backed drivers, your connection needs a user with read/write permissions to the database. By default, servers and tables are updateable if the OGR driver supports it, but you can turn off updateability at a server or table level using the `updateable` option: ```sql ALTER SERVER myserver OPTIONS (ADD updateable 'false'); ALTER FOREIGN TABLE mytable OPTIONS (ADD updateable 'false'); ``` Writeable tables only work if you have included a `fid` column in your table definition. By default, tables imported by `IMPORT FOREIGN SCHEMA` or using the example SQL code from `ogr_fdw_info` include a `fid` column. ### Column Name Mapping You can create an FDW table with any subset of columns from the OGR source you like, just by using the same column names as the source: ```sql CREATE FOREIGN TABLE typetest_fdw_partial ( clock time, name varchar ) SERVER wraparound OPTIONS (layer 'typetest'); ``` You can also explicitly map remote column names to different local names using the `column_name` option: ```sql CREATE FOREIGN TABLE typetest_fdw_mapped ( fid bigint, supertime time OPTIONS (column_name 'clock'), thebestnamething varchar OPTIONS (column_name 'name') ) SERVER wraparound OPTIONS (layer 'typetest'); ``` ### Automatic Foreign Table Creation **This feature is only available with PostgreSQL 9.5 and higher** You can use the PostgreSQL `IMPORT FOREIGN SCHEMA` command to [import table definitions from an OGR data source](http://www.postgresql.org/docs/9.5/static/sql-importforeignschema.html). #### Import All Tables If you want to import all tables in the OGR data source use the special schema called "ogr_all". ```sql CREATE SCHEMA fgdball; IMPORT FOREIGN SCHEMA ogr_all FROM SERVER fgdbtest INTO fgdball; ``` #### Import a Subset of Tables Not all OGR data sources have a concept of schema, so we use the remote schema string as a prefix to match OGR layers. The matching is case sensitive, so make sure casing matches your layer names. For example, the following will only import tables that start with *CitiesIn*. As long as you quote, you can handle true schemaed databases such as SQL Server or PostgreSQL by using something like *"dbo."* ```sql CREATE SCHEMA fgdbcityinf; IMPORT FOREIGN SCHEMA "CitiesIn" FROM SERVER fgdbtest INTO fgdbcityinf; ``` You can also use PostgreSQL clauses `LIMIT TO` and `EXCEPT` to restrict the tables you are importing. ```sql CREATE SCHEMA fgdbcitysub; -- import only layer called Cities IMPORT FOREIGN SCHEMA ogr_all LIMIT TO(cities) FROM server fgdbtest INTO fgdbcitysub ; -- import only layers not called Cities or Countries IMPORT FOREIGN SCHEMA ogr_all EXCEPT (cities, countries) FROM server fgdbtest INTO fgdbcitysub; -- With table laundering turned off, need to use exact layer names DROP SCHEMA IF EXISTS fgdbcitysub CASCADE; -- import with un-laundered table name IMPORT FOREIGN SCHEMA ogr_all LIMIT TO("Cities") FROM server fgdbtest INTO fgdbcitysub OPTIONS (launder_table_names 'false') ; ``` #### Mixed Case and Special Characters In general, PostgreSQL prefers table names with [simple numbers and letters](http://www.postgresql.org/docs/9.5/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS), no punctuation or special characters. By default, when `IMPORT FOREIGN SCHEMA` is run on an OGR foreign data server, the table names and column names are "laundered" -- all upper case is converted to lowercase and special characters such as spaces and punctuation are replaced with "_". Laundering is not desirable in all cases. You can override this behavior with two `IMPORT FOREIGN SCHEMA` options specific to `ogr_fdw` servers: `launder_column_names` and `launder_table_names`. To preserve casing and other funky characters in both column names and table names, do the following: ```sql CREATE SCHEMA fgdbcitypreserve; IMPORT FOREIGN SCHEMA ogr_all FROM SERVER fgdbtest INTO fgdbpreserve OPTIONS ( launder_table_names 'false', launder_column_names 'false' ); ``` ### GDAL Options The behavior of your GDAL/OGR connection can be altered by passing GDAL `config_options` to the connection when you set up the server. Most GDAL/OGR drivers have some specific behaviours that are controlled by configuration options. For example, the "[ESRI Shapefile](http://www.gdal.org/drv_shapefile.html)" driver includes a `SHAPE_ENCODING` option that controls the character encoding applied to text data. Since many Shapefiles are encoded using LATIN1, and most PostgreSQL databases are encoded in UTF-8, it is useful to specify the encoding to get proper handling of special characters like accents. ```sql CREATE SERVER myserver_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/tmp/test', format 'ESRI Shapefile', config_options 'SHAPE_ENCODING=LATIN1' ); ``` Multiple config options can be passed at one time by supplying a **space-separated** list of options. If you are using GDAL 2.0 or higher, you can also pass "open options" to your OGR foreign data wrapper, using the `open_options` parameter. In GDAL 2.0, the global `SHAPE_ENCODING` option has been superceded by a driver-specific `ENCODING` option, which can be called like this: ```sql CREATE SERVER myserver_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/tmp/test', format 'ESRI Shapefile', open_options 'ENCODING=LATIN1' ); ``` ### GDAL Debugging If you are getting odd behavior and you want to see what GDAL is doing behind the scenes, enable debug logging in your server: ```sql CREATE SERVER myserver_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/tmp/test', format 'ESRI Shapefile', config_options 'SHAPE_ENCODING=LATIN1 CPL_DEBUG=ON' ); ``` GDAL-level messages will be logged at the PostgreSQL **DEBUG2** level, so to see them when running a query, alter your `client_min_messages` setting. ```sql SET client_min_messages = debug2; ``` Once you've figured out your issue, don't forget to remove the `CPL_DEBUG` option from your server definition, and set your messages back to **NOTICE** level. ```sql SET client_min_messages = notice; ALTER SERVER myserver_latin1 OPTIONS ( SET config_options 'SHAPE_ENCODING=LATIN1' ); ``` ### Utility Functions To view the current FDW and GDAL version. ```sql SELECT ogr_fdw_version(); ``` To view the drivers supported by this GDAL. ```sql SELECT unnest(ogr_fdw_drivers()); ``` ### Character Encoding To access sources that have a non-UTF-8 encoding, you may need to specify the character encoding in your server creation line. OGR FDW uses the transcoding built into PostgreSQL, and thus supports all the [encodings that PostgreSQL does](https://www.postgresql.org/docs/current/multibyte.html#CHARSET-TABLE). ```sql CREATE SERVER odbc_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'ODBC:username@servicename', format 'ODBC', character_encoding 'WIN1250' ); CREATE FOREIGN TABLE featuretable_fdw ( name text, geom geometry(Point, 4326) ) SERVER odbc_latin1 OPTIONS (layer 'featuretable'); ``` pgsql-ogr-fdw-1.1.5/ci/000077500000000000000000000000001463733357200146175ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/ci/gdal_build.sh000066400000000000000000000004661463733357200172470ustar00rootroot00000000000000#!/bin/bash GDAL_VERSION=$1 echo "Building GDAL $GDAL_VERSION" wget http://download.osgeo.org/gdal/$GDAL_VERSION/gdal-$GDAL_VERSION.tar.xz tar xJf gdal-$GDAL_VERSION.tar.xz; cd gdal-$GDAL_VERSION ./configure --prefix=/usr --enable-debug --without-libtool make -j4 sudo make install cd .. gdalinfo --version pgsql-ogr-fdw-1.1.5/ci/pg_hba.conf000066400000000000000000000006031463733357200167050ustar00rootroot00000000000000# TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only local all postgres trust # IPv4 local connections: host all postgres 127.0.0.1/32 trust # IPv6 local connections: host all postgres ::1/128 trust pgsql-ogr-fdw-1.1.5/data/000077500000000000000000000000001463733357200151355ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/data/2launder.dbf000066400000000000000000000004501463733357200173250ustar00rootroot00000000000000_C2ameC2ageNHeightNb-rthdateD Peter 45 5.6019650412 Paul 33 5.8419710325 pgsql-ogr-fdw-1.1.5/data/2launder.prj000066400000000000000000000002171463733357200173660ustar00rootroot00000000000000GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]pgsql-ogr-fdw-1.1.5/data/2launder.shp000066400000000000000000000002341463733357200173640ustar00rootroot00000000000000' NTC֗^JF?,?? ,?? TC֗^JF?pgsql-ogr-fdw-1.1.5/data/2launder.shx000066400000000000000000000001641463733357200173760ustar00rootroot00000000000000' :TC֗^JF?,??2 @ pgsql-ogr-fdw-1.1.5/data/Querying.zip000066400000000000000000006153361463733357200175020ustar00rootroot00000000000000PK qE Querying.gdb/UT \T\Tux PKqEWᝦ*XQuerying.gdb/a00000001.freelistUT \T\Tux ס |tB K $oPKqE˖Pn!Querying.gdb/a00000001.gdbindexesUT \T\Tux cb```b7xO 3I& qC"CCC*C1C% A10tpЬpW  M8PKqE.aQuerying.gdb/a00000001.gdbtableUT \T\Tux m 0 VT> Mqm|I$9TĜCTys|^‡k+(h+\)|Lj:\wRX 7NErK%kW΄UBo ]̄~L,5MU)1&LFh}_2 6zEQ_*ʖxңa$~ PKqEUH Querying.gdb/a00000001.gdbtablxUT \T\Tux A 0N@ Ph@ V! %M:ED֖\k 9E+yl\{J$I$I|PKqE(2'Querying.gdb/a00000001.TablesByName.atxUT \T\Tux M0G/ b&jᨾ &2ț6ae] 3ǧY2+fd c |^,.DXhhD.qe==u8rW6O?C?id7V֜.:iN] >Z&͸2((NgW/@PKqE0Querying.gdb/a00000002.gdbtableUT \T\Tux Tk@>t*Ht7pI-ߒd$neD+nUؤ^lzSX V~Y7BPB(EzQP@ʑ<5)P.v)!%Zoo7ƔEt1JH9S.4/9qrW?Q]U' ǝd'q_<`ge ֳ{s*GWfj){!zh5s 3Uôp_qhE~[X Qi0\DήeyBy^xsCI:NRꭐЫ8Y$?o%e#Rl/6>Y[ Oۺ3=uq)_\pzQ}qe²&M=O2m\6 8 E36w-,kyH'Qr5ї6ko^ }V5YubaS Ƿ`~x[mC?UM;X q4N{$wtC-}PKqEzA}n Querying.gdb/a00000002.gdbtablxUT \T\Tux ͱ PF<^  8D(! 8}VpT49TJJ6RL.ITGF D&!ғ;yoI$I$IPKqE)!*!Querying.gdb/a00000003.gdbindexesUT \T\Tux cd```b7xO 3I& PKqE9E Querying.gdb/a00000003.gdbtableUT \T\Tux cf```@ʀ,Z`!F&O6&` W T"rR " +!!(* *G/T] 7|T"eAaS3T 6U:/s9s?뱅OPKqE>;D#Querying.gdb/a00000004.FDO_UUID.atxUT \T\Tux M 1ഴWN410goܡ[.\Bu?5_H٪ \%CB"( jZLV:5 t$ġ\,) 8fE}[.gYfNd8q On\8*&Ok([.SR|l7:Ysԛ#k ;zRF*16xb˨Ciɵv?OPKqEY@(GQuerying.gdb/a00000004.gdbtableUT \T\Tux ]}#Y${eHP::ABv/|^ߞ^Y=wgf@A4""QQ>R"T (@jJT>D? TUΌ=k{׻}|;y}'GNO|~?t}6|H8*l&K pԽSº};UvPZdSSCy,B7T*, . 0.M#,L 4udM:"LݱOiܷ. ˭E#%!ԙE(` ȗ!ͧpȠ=VN8^S3i42S !gY2ª$煳N,;,$0-3P8bn֏nA 18 o|?6O^[oFkE&zz'?kړ:1޸7_Z|Gw=Gx o ث~Kŗ#bƿm!؃Ϯ=]ȧA攴iۦri'_ͼi<'nU',q! lEQ FBpZnP0XlQPYK]jT w[l=?jh&Z6XxT( ۮakskH+JihO-(S}E6WthR9eeꥧ2bvQ vTKKy~l7s]z{uw >5'iIu6)k4U%S#yoJby;`K~mGJUdS>rzq?5l&T.ˌ]ˌ43k^fZ_fd&7` 14Ԝ,e5Uq<&|jjXe_m e:!nI/x,5gl8"Ph!._48N-eR]\c*IfMiU|8^CRT}CVa Jﭒܒ}db&ma` f`˝ӶS`q'׷zEOGguy`2#;0{q?svPыVݑ;w.|Wڏ?m6f^:;wnOa/UOjr |L6/9bp`zrDbKL/;ۼ0rpَжn[|ײvH|^6X`$uf;B]y9/ie> ԟI!bm!dumۅY?]س &tz=7ʛ&tIԲ6 ndrY\M+|eT>yN|5ѐ&mĺV:Uc7ō|$: v<N<4{)^y<49,ic~6FѐKahhSeo<( !5zȺ*xzjʧ.k:LbiWIƶ=.o"ZDN!+%ۍ6]ޣ7yS)4`¦|CR =Vj^n_VeJ)IHGR>򑔏|$#)IHGR>򑓜7.^5L2 7T%*)UIJJUTeJUo{DN+I쟢%%()AI JJPR%%()AI JJPR*Aa;m`q+`)r<5x8 bRHo8XHDH$4E#y)]B< E0vD84%bVʔRdJH&O&/OwdO;_ߟ|s_z;}׿R/>+uA>{wܱKyw*b/^d^YYpX̴<唝f2wI<_gUycLۻRªXX!IoXRh*QOJ[hlBfhJA*äfFR*+&lO7RItp IӊkuX8dUS\c&[jʲ KT\*2@HTD [[m]q ր#! q.U 4 7Za96?ҡ~pHI?@ r`p{Vq.2NnkuTRJb ۓ\ 'X3$Dk/g7r ~%n`pAc~:Ԅ=JZ0urJUPV@ 'ώKMJal pXvi&&D$pBpLc0<,n)VM\6]VqL@Њ0ڑD;xRܫ-b tc;N "Cb{/xWȘٔ@nlstId5q}Mjcyg܎6W2)YuRRn_FSϤ|?AChFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFhFh6Xɍy>yщ&^bu_;u{d4!e4! -ID fn WCr#4Bz1W.m]!qFCWwleFN h|qp>hn=ֹjzwFB;h~3fF޹Τx5~tJhG ̴_fxgή͏@hGͻ#^f@h''tgv/B;hnxΘO4_Lucw4Ўޝ0{4m9N<=}vt:vh.k/3p_3h/{axba|`}ƕj8mE28keL࠮Q^uˠlh9 E+]Zp=؟wWWѽmPǙ3L{Q",Wݽ ~ CvмݧzޜR&<F}򥍶;&ގkI ^޵ߕN_:N=&ooc_sGhn?{l޽ݯC:3ۣonj{ɰ7Q4pT9u-otw\3fw[ 8*kﰑ6ݔwn~ۛ]_Ad+]=hq#6ڝm?w#N m7:tDzG<܇עSozI9w:xǶ{ F-us\,֐u^IqK/N>8:=_pƕ<>rxo8hw/ஷBtQxD֎OzwYdywޟw>YZ!{hvąA>}~ޗN:]z' nق~0sEk~ܧkW mW'F^~mT%hOk75H v's\/D0h9eٻG>B s@P .է-쬪5YCFY鰝Lof i8! B8 ž@hL;]vMvW#٪ȈEQh M)4BSh M)4BSh M)4M?*4uоyYMG+n M)4BSh M)4BSh M)4BSh M)4BSh M)4BSh M)4BSh M)4BSh M)4BSh M)4BiMLӛOЍMm%sh֏<;ui4GI4JK?Fw {5`WW4"q\#lI P)U)Hˇ+#+ѬsttY[SpdcMyڿ,c ¦v RXwt=rlȮ&|eMUjZ=͗d]zhl޶] Y 5O}XifқY:7Sa~ sJݗ1D/]ش|FS2WzIavuCi ,PwMeVeҷSǰ٭ȥo2h%в-_%3Lǿ`H\h\ٮr[Zq[r!w_֟qku~xt8jZ];'{tjggԚF8?kn"r]qݶh f)vAt!)-ʖ9=;.mR3S]yKo8D<[ {xKTL/4]>Ac>|oޕM& __WN)RxLv^;mUk{zOvFkt?zSbaa^\Sv֪X*)(ȪAv.(mgh޶|F@6mH,8x~ɄۑGWkG~P4t~W?6ڧI&:ˢWO9n*v9H 9|ӷ.;p ;^6{qݺ>yS}`^'tb9(Fdp>4J#/ րmtxx-MJJ^~⣦G؝M|OB1fdyS}csOk}^!05]Al"p3K|9txz~w>#=_phxna?.{"(u[~=0,4E&MTIm\-'q^GξySXf(2<9|oRޔ.{SANT;>8Dq)E2ׅZ^yCFdV~rsz~wzm#8=88YuS,VIElUP} uEݎ]nzD8m(iꤌШOaV;h=2c 9fཪT  <鄥Ij v(DOC.q"XYeiL! "Ae:*~\TWE' s(y;טQNe_y6E 3m]gA6&rzAD:;#!d Y>BV=S᫏7\+q}؟VѮyP~*'kMƮ65Z{u?/8/!ɴJ%<") |UO9@X,J oZ|x&%x-!I%vi0F4[$Q8Eлt C8nM`$O.F~kl^2ڬ]{5e97+(ؚ>-9&+fN2+Lq 8Ma5u"3o#QV[>Dʊ!G<Lǭz⸜3uS{FʑG 8 ",# 6 #M4QM}uZqx2$;jqGZ=>@A#o엏i2^  rSCLJlaiuet|.dξ$D.M(;M),4_jxm%W*ϼ,ZheȖrl|:pn+Km\p&M>0IF{880XБjˑ9!Y.R:?g~vZO݅ue6="h$B@;#D޾]$ʆռ9W}%7ؔ3qڱ)84C2^5J[^9p<ߥpS >|ʢ \[OWކ]!"G;W\|t8rG.. L&ɳJ&!/QP%s՜ȳ(LOnA(m KЧ]-p CE"x 6fB ӄ;T0yƔ m?]S5wr`)EKwޡLg(B!Am$z?uŇ~6g A}*/?KC21&ѽ-K9Kb\i=۠M˰k^/;FܰA;lɳ9ٳaouŗJm- [v9K>z}'Ka]p0ڶ@z5l6X){fְ.-:V YKS[Hv)0{W蝓Lׂ(=*ɲżEӆ/6Υ[o8c3!טmE+ !e=Ϥ=.@Y9+F{^v&cv٣HJ2N_0;w uG`HN:c.;֠oDDwo* J<-߽?]<jR:g5z--/ջ Kfd&ۚ0"z:}BXYi>W]memJV ز#iG^Z7-l8qe  J ׮s; ۾~BI?1"Z%<ڗc:}a]I.#ēw x)OkĢa;/Z."K3Qād:j6WS?hn9toz#1KB qjWMfw÷  Je/d P F!bN٪ZZ^E)wUj~~d|!iv-Hhʮ 6X4n+n\hܓ$Zʽ)+ ]lVje4ڈ!S;({|W(K$Bg8K\nl/9"1_a&'nEOsGjg^l֛nZVJw& y{F{:"Ԫaï*/`d^Uɱ5doU t֎O=xZ))<=Ut=ڜIQEA, bJ EG0]ˍ̀]m(%wH,HP=rbX,0{#h4nƝl *42&$ǺH(w8ۥ} z>]&1qTaEJT9<F' wB -Oϲ;Nv~zsjK't"&$E>JwZ} Z?PfQVcrV1Ovf#jq$80 *H5 yx I(7g0](mUDy4 %n'<ƷAadgcnI0[d?r^(Oc;Lѵ-RK{asX,핪#EIQDp' E3kTvL6 L--'o0R.?텫R\-/ ʓ֩'^ӑ=x#%NQ2Hqؘ@QGU* ǼvEH`lEOq0FPV!cArbfZ1eߋxowmTWٶk2KXK2.UM't؝ ZTY #(VX([eTDh]-B{ L.'ʱA2!DAH1!S!"(DhG1l$U70y:D>dF^L^Ёޑ8 JUS{੡oul=N08BRI!+unYv+y~KFZ@05yr,uq{bh\xJW1ӀrzchZ|i9d[* |p6tEtRLXl1^@)I5*,ucqϷUAwՎ^9ֻmrZϠJKhkeы)AM۩Rm3ϼE[u{w%"Y@ erfpviN~;E+R%oARfw`b-,\,m,k7-db&ݕٓGʼ?̈ lh #^9/l 2J,+Ϗem#&s~\[^{Ѥlxq~e*W%nSgFR.8['f!-#\ʄ&?lpE1;Zfё|r8!M3ؑ: t eI.N:,+}db皰q/'X!Փ`Ui-LJf\7+y@c1XybM"lt/wug#aU%4O=6O 73Wz#rlsJ?g'OՑŧaf-[f{c7.F[8%wMKxBƽjPc ƃTLgžXpȩKג/όBfyͭ18CROިǍW&|qCGY[uHزHWax%0Bs#B o[~Z=+e lwc?ƥGX TQ!d! ` $ǤXǂKxp}XláYdAn.>#[t{{#'~[`9) KxBa7>Ƒ!+Ouh)upĭ P?׆>$w1%}528K~?xHTڀ c LC΋fP6~^Pwӿ\L*&hJ2 P|: , /:dǸGދ+c)SۃǬ&{c/ҝZJ]aUW\o*/T芦wJ[3xwaNrLnP" %x=jh{xfݨՔ,gpe2 SU=de=7ʝsgQ:r)Qr?-MSD3ke##3 5 ̊v㞉/-ٗwDt&iJZP@:6{($b<+t仮E2 ~pZ&y5}O hD2M\>`م5 -<܂[C}Q_Bqng1"=Ûn[ͻh]X ݼy馉`\~`x"OHЇ)%uL94dSx6 mf9~DRp q6-uz=5@4.&NӢ[g^^#uX#G01Ԫu}fdN^Y2c FMY׏rT3R^eԳt*!8TrBҰZ}`BLeт.چN4_JVPʛVZ( )#p_ƈW5'юn/~0~;@/A/G!E[HcOȟK83Z~zH3r U8'ݩy/¨:G9JNwYU7s~?c:Կ4FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44ڗ~4ZgY4oo=nM +sǼEkѾd4FhMi4)Ww%"?齆z4Fۄ, ΘWֺ=nsFCq_gmjVeFijO -MGS阦Z,44ڧjFfy.t}4ztl]myWKit&m%y6os^ZwC}m=h7En$*Nh>Uvjܥ$T˛F6MT9 x3~Y&A{gO=Jp]i-!Wnck7=5BȥYYwVFEyM~6uF|VYkaλ9蹰]4k䂲0_vQHϏM52hI<Μm|V. ܶOm\ei>d˱vVGƲ_-ʶ|L묇Ztz%D-SH/}vVK/_vV|/ Gۗ4jH)-kBa۶=|I7bB煦$)9_HbI}Gz_ԻuFZۈmrې멾~Iu绯44燶|;m] }ؙ_h_ڪ4i-oTo:^$>yN)O6ҳiY_hC;wk۴ݸ/_pS=UE- -Z1w춹0;g-жK~w/+[=.7XFۆFoKF[>Co]|/~<4"Zzs;>=MQfGKC9/u~wY-en+VsC=IQҶ'o>{/t:_{<?>_Z (}v]}5 k Zg1>츥ϕZӗ@:NCž8~8?Q/bamy:!}6ŗ}$n1w}ۻgI*-|WJo>>=w+>̺oۖx_6ݡFKp7͇wnF61lB>K<]DZz?&}v j4vEkͅASkhVYu=nM݄FhMi44FhMiCIi>TFhm鎺}Ѿ,4u3GY!i4v4u3G[4FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44F5F ]7Mi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44FhMi44Fh(4Ȭlr ǎgaCVn{#fp <`YwWZDZ?Gl*dߟ<c=9OlK̴ޱ3۬X/V*Kb|pxhDNMo\3\93Ǽۜ]?;;4&Df-NLg !v((1?Ԍ3԰UR L2%/k7yX3 ]sl=9,WFOգ+lkR+bѪO`EqњϯtUS3O)3ǗT?OgkT!- F,3A/KhOyC^iĶ3QH'd`T6 pkgs62ݐCt)QlIs|9ImH 9 $ZINR, #X {P]EMF!l8p:\z(ChrE&c 9IU.d'iU$~!Ygus Ry 1rѲKAYv< 2yȼQ0_7 mt{S"ks?Qxr;ΈԕYgg~g3"uegDٙ3 ~բ&37wE|̃ShE?Zf'+%2ֱK>5NўrR:(UKuX7{ǭƓ"NRDmʧB.™=Yay)9ͫ m̀eF niIzJi} MҺ`)Z̛xeykTnRq'w* |AOɂ\~$eB&r88 C1ȆDPJ(tɀJӿ 8١R)?P .F`"׉I57,]x?8H,ИG. \}qa)dz_O*Q ܯGj(P7k{aZ,AI(?1X Sߒr3b#O oש7yoO ED,|msXQpx^G`e. šSҲBj(,T 5#Zc{BDCf\xFbh0~mo\Pk5Gq J?V^ J*Q+lұ KX0a2D7ٳlZRL55|6 _WX/N%dKHㆯ˶OTP*]#?$ IdpRMٓ )3BVRuD1hr05cPp.Yn3@/fDz0Q~]9iWPC6}w?i<} ɀ8a÷bm]Oqc9@RBbbίߦ~ah-(D>AY)֗dD Q#!:?v-^LVֆ2l7?hlȯ#VN rݪ!9p *e 3vr {׀U Dk2`z"DU9rU3E& M>l[)jam2w5rtT,,zp;0fs7>`, (_WO/"gx$rurO#ёaQ`ZQ z n=?O-CJo,dEf0a,AzDJ!LKgfmC"uL%ڏ[G$e@ p<$JBxc: ~WK&xL`*xT٩n`E?MdX b#M ک,cWcN]AbOYi)'..8Z"_,FaEk{@s[-J%\@#EB#3 !iV6zJPwʴVoF\A0򶠥SWNθϔ)0 :dypu%cثAF 5j- 4!7F:E?@klȯڃ?vltsLB\dE] ]dC3گBkE6;iK M56|('[)l}(gЂm+"qӯwQ{b~M&ym*|$'mV\:9YR 3rb 2KJ'O>-%Ro"9xX!Ӳ,IQnҋF>;u,0h̘$*Rcr4&L46;̍-9JN`DsAz BsE%X0g3R+[?~#l)4/!+L`jم2h?><;ZO̹ vib2(pg{HhCl7c0h3ȫs׿cNEuu5Fm/utf dn4f'PR`"MMȾ =#ir03 0gDpm8Ё`RֆjD;ϿrBu4U)~, i٥Ed< 8HF5!gA18EGo#~AsiєMWr ABI<hyB܌,q=F|LgZ ^S +  . 4Ŧ+0 2Vm6AA{02{ou7g]DAB-/EF1mZlValP#q8`lf}Xgn[a:߮Xb|ʰiF~;|"ZX N߰znVEk4!lz;`^ź.$ %ϱfCZ~?no]uZaܜG@j5և^w,)jbcHﶻ0Wgg@E{Fo Ք=i~9ЮwД.ؖ;x{uڄV|W3`['8WS*BƵΆڼ56hQ%^rl0Hv-VK{~m`=@gмaRk}6sJ@ppڝv ]4;82ÞhU0i/~>mC`N}v]wJ-a`::)iaҊ3}T.K`]$VAeB &'q AmqgBrm]j}/CB8ը<p3SVHbl K0'㓩ۘ>kxmh%vZF:{1R ͗ qDɑQ([)xs̥FXxgBN$4U3,r?O}7eiD!rsB9HDMmC *HPJ xso|[y DFv6f D E)l4#eŇ`Tc٪m:ab'=ޏx (g!F~=I0y/35D+h9n$gq(t *.pPN/c/Z=;lPE5cس(Bm1]-9p (uRMz:K]`mU]`Wn[oR%%,??:P?X^)!LVj{T3/gі{E,Yk, Ҳ`ҜvϹ%73|GQWJ,כ;sh6f%`D[0XXX ҮQ%[yMaF\|jÈD[=G|fRh ӡtoE&>Lj0V^ch/-?^_J录ԙ ։CH&2!Ib=mE&zDqj8yG'ksmdK pb,v%_v_M+]mNwyݷU|T][U΃B `+6Ah$fլF,al` V9u+qy{M3W1c9jbqa-:2"}m]&6T h+w:}ex2<-%{RuօVtP\qN poJKϦ7ݕs{N(,!oKnk_#nܟXO:e:iԙFM$Ti}|a/av.ͰǔKi:_= xJCvEnCK!̡l%iqnM@.7&ʎ$v -iT*>:Zly}~t [=*hC)ңDQrtׇc%-L\|Wq'MGU_vcF a|wZYd ZK]8DG hf?2q'8: 1woԻwD-QuGOPpc)7\~*V5  My5_4~ÏӨByS:"Ưq4,RuO Cv&U: SZ`Ms(K=+= 7UmbE!9k4l*+mKz+&Ov+[ϴӜ'o*sj4sHY*g}bo%-\@YuZ U2x^R|:jFebEh҄_oe9{b\%,m/9Z(ڲ{ۉg xh\ Q "w1,}ϵ`l'޲ʓz1'/Q/Nޛ}+DWv&{ %4!>V(!"q'尟T>}&\9fF{J.s'"{ڣ4 Q6&dhtS~v ,abP`6XV;;f\-5 > x;cA=J CM/5T;'ι+ ȃOIGupE8yH*' 1>s %0X5vx lzT?w2TژoFA۴Q=xᎶoߴ| wuXg!`j;жWB5M^I9UT-%#ኆp:AaIXVx;A )m:f M%TuP ϑq.d@tP蓑'ԀV4u Nz ]Pu/ceVW}b g#=Pl9RBqgܔO^^͈STio񷋿+^Yo vE%>G4hD#шF4hD#шF4hD#шF4hD#шF4hD#шF4hD#шF4hD#шF4hD#шF4GZ)߈FQ mKF4Q^]gvã 3Mi(߈F4hD#n3 P)>pW޻{hDF# ָ7euMyZ)n ۋa{FѤ9ɯ̈Jhbff-ȖtVlL#ڨLv{iX=YZhܒ\Q)ɸ1!e&IxT D1axlR{R ]U㋎ b,iR ݤ$*64)nI4mv] Ks&u{hH+Foy6I*oV&Qi̺#H&5y6,5)3ΐ-\~oiNnN#$V[:5Tp^3m.{YbdYվ9"eף2<ӸzpU{RHjh-Ƭe0j|kKla)~Og;կ>g?W~Z|r["V n(3%sm|{{hI=fHsqޛc%ҙedIj.jjǢ^]-7iu]sI7TC6#gGGMi-\-K| ǡMávhH#:4zׄ\;q[ü/1'&-I:S.t&͈/}#]:LmZ4;W"ڐ4\1ivh ]qu6t-:n %̈́HJl-Ji;ɜM;IIڳ踩sa;5< lϳ6m~3[M:QyJඞ)5o.v*l8_T5b vѥp=+UE|{?vW&ݴ/ nm V۽j"ztZ#}wr~]4)n7nnPwL~0?LF=ˊ}!Ҥ%g40kPCHCCIziĨhgZ~U{bNJܾxS) ^cvsbV6<76 ]ZnzV2LfgE\~[<пos?g_}ow޿;O~O+?~׿?Po/'q?fEKǸ=jl[{t?Z<0||icOYF =i>zmpV]Y\?0ž{nEi>w;ա;>.oe%yZUmC1v3&k[fgwܨFɻ QsG .¨֡%@a+DۑHӲ}G]^K߃ŀ_64y:تb`*P<|5Q_· N܌ElglNsZfb7}ۃi^ZF&S݁e`LGcb;Нv}\g;q ٭F#L#nfb}}XBE ͓Zh9=I[N̻4y.hR$+-VKKX+.a{ 56S[,l5\@ZѥV,jwm4'=~<`>&voc<  7~e$jɰE=2;W yzف R:10PM]hw^gcO7>[?1s)1GRB๝vXdxB"cH%8 gC5PB`)Vnra#TYY|Ybe 3Q2Q~J9##nٰ!+\\Kndf23,i-L)[gdiȣ4A!o҄#˛zMG?fͪka3|TY_BPszR) C޸d 3(|G+Ӯ s=F̓Zc;`>;ew77Eu֡ov{b8ϼst̏Wh(Im ~e,LWrAޅY&Lgm f<0qk[F] tg[uTY:7~ \OuFno,*v ||QۃO}w 9bKJՎ:}}e}5:(Vkl~-YʕZ.6NM ǘsj[NJwHڻW[ 5b .e +rs+0wKW{[G\$Qʻwizkfϳ۶R)]R~%Zqox;:Y)-Ɋ bD w ;$sv9; Λ FhTLCȮHJ.]c z5sJ/#Ո˝1.wF\la)vMY+vLdGQ$tF*>c +0/ t=#HƝ-Zvn߳C0bW? h^ܳ/$\CC?n23{Kh W}Ki8b>O%B>*Aq;=܀bq]%6F~ON%(~qPi vhi{~LYBr{S_-G(/p>hC@` B6񿣡4DiDL8_§ѠV2/' Q9 pehr\nPRTYu;g"X`#'ZmnsuɆ7WXs4E5BY.(4KVF\ܿa>aSv~q:~7 b:ΧWV ]Fgj|03 ̽c :_%_s[Zp̴鵙hpzjǶ JZYcڑ鳦 Cwj'X|DZs`:?_bQ^>KLLN&FјXbh@ =tJ2[HHDAl^BzQ N5m1Rq$T>.Aa]gQ~V OL"NcKÍBf˱@^5zGzu01#I0Y  ]8_ȤfmBL~o糦͓13tf0۸h^ɱh|:1Aְ}Bx斂i -V>`MM JcCL\^+dT?`K<ӵ'8{OaAAQ{PneTKJhO6Z3Ee)=`dp\0(a候FM* `w"DP+gtzM}σyvTc`:}=4fpN@JGzQ!-l3;tqsH k 6kͶ ٹRcKޒlsu.<<<~Yku$`HJaYgvZr?] Xt>>PcɭbGgv܀EuHPa"a^}ux }5{;*F'vwmU zXP ]C7D{woziG;tIW`eU\*.Ɉ;7b/f |&7*>ťzK ozǦ%Y 4td$4j?X frC^o ^vK]Jaov2(E]<؎MKe`.,iS&!DS؏jiC&XCW\ E߉=-J^芞CUu  #F=Rah=KSqgql)tH! U4XYqcG#}=mPͅ䳁,CO6~0v!r]Eؕ"ՓH.yN#lA{m_ _o؝JLl8b6%#t Gnu(Q 'QaGQx6rр1Niy ?ƐMd m' -_wa(PzΖ=`Ġ]'.͑ؔ]{-'b@!JL* E#\$GI81qch49 D=q 67b‹HKbj]v+,c_+(M=5y4G =>J9d0ڝx%9^P2˰͐M #"iBrhDd `c6 =@8MUT7?H9ۨ. &>Xac LzHmOD?z a\צSym]@Sf(zyK4!]{v4S2.ubUQ)ʢ\fܪVmʧ(7En |vVz_fEOjݪCͪe~_/ʧu-yܨʵ%YUuqhQ䖨TO-h4El4@-Ӂ1/SqVW~?r֨['/hzѼ8Dz KtX͠7MR8jKB cJځR˔>7.N+Ћ/寖jL.ʧW=o$ՄUgQ>j\VfR_Zw kekfmm`[нVkUZWDOɩ֪PT#qzd1.m`x oy mm[D7MzYA>`ͷz%mYmM isܰZDAf AuqdjVU|j4Z%ОSYӪ FGAW% bUq 8CvbA}_ 5h.8. 悛5-avi!fUrnd (Q55 UJ)"fUIsBCM-h*k4IqIL^J -s īZȦ/ KVMQS-x R>?\[-yR f^OXC2HV S=4\H pn\iH~|?f~`ͨ&^{FP7A6^049A+RCIJ+XΠa#K˫=6[T_ )V.*'T&8?ìqPjZoߤoV2>\~##5\~Rr)/nC\1R+蛥N֨W'U֪t(|f顔^Hhу˄LNųmѰG2dg8ǙǸqAOQ >ram)l@d6z.n7H%vZF:{fg@ qjyXS"a\1l`b;9ܥ3N8)0BeQb/S*: Ax3wO# La%.SEd.¿ֵYNFa)[ZQ*D]ފ2$;Vh"ٍ # 36_q-Npwoo`NV,BQ JC|G6Vhߓwobāaڨ\/!BAعnָ>D1-'pzKBЃb^= jZYRt<SOԱܵM4b%q UR *!4O\%sMy6t7Npæ&8ǹ ,UMKI||+A73D]o3! נ$hV~N{o?Dɂn (A˪ۅ;N^Glm|cFP#jɁyF.ThIN/h@uǑQk%斈mdF*z!zQV; 6.q;gRõ`;h!{iEiuJٗQ֊&Iͨ?]h+e+JmŴG9H"}EgC*͛w Ͻq,QұHWe~V-Hƞi3Eye@V](l(PءJ US6Sy#.QWg֞+]ґ=x#T]*HpNal ߖ6qܵef6QzTSpdj};}q"P4ki nS{9ȧsBeU4$-0ĘeGhG]`UKZҀ٣Q4R!T 9TJEOJpx"Kqrm5@Юt, u.}zO9 U zwo$T_wTˆ' C$#j\ P+1)+(8='"boLht^.2u WMK't؝ ͚H̳$:KPPTѷZhmt+RBȭi0[ 1tk(d~|}PX5}WG*h#*\}LX˩޹%bi4I=J7)c:В(`TO }3mg(/iף'OɧԚA3^Iy2 ȵ;J.5ytxlw}l_]r#Gev <=J:=D1{4/4ӈ$iB%})ahZr{ʝ+/ҷ]s?2t{`("Sclx <n2]WCgC>Wj>jGaߏn=ۺZ9 g\W#D#Nd`p1 0 ғ (y@4( t,(%q9A.gwԕ`PTDSW#Z J %Qh:2M`zGG#+&$*>,XZ;7F|]j8/cXgDK9I[ɤ:I5?JS&Ygvn SP[#Kp<4[5 .Mnz|++ޭwދQK? $RۓDۢ<=Kg~R9FV^ ztئ"jelWBsdPPvҌ1x;J^y{@G/Ȟ D nPz^q fGsK#&8eBAR B!C4烨~ H7%H.U+ؚdn!^9PtGS^t Uf3 1=-Sf ۸l%?0 O\T^Vhˆ By~${hru3. -&13rw 3ȟ랝\5qQm֛}>eĸSTW ~NY2c fM٬8\XkWw4;J%'CJ^_*c)YF:ߠo脺M+YuB30t! ŵPD #p/tLgTГ%K7p/A؂kU&?>蟺g 'Ύ'ނo1̤}Z]|:!mo1/^ŷdq99߾2|ſ=*]oG\^"?-'(;{MYFr,6LF{hYKhhhhЫS^".%yc4F[f, J[g։=g4w:(?}3RD1Miok544Y{WMgreT`6Mbj3) F?O;#%mLSF{hͯ_2ғ:|z|wmoj1eW$lBjeyxLF{hl̽Om|i|Ljm۬ݹyl\Hh}cf:ΊXVVD_ߥO(/vklY#۹ڞzќO#ysϼuCI=sAK#QrֱS[U<,39\ܷU摹MӘ,CqhGmz2eD~f-?:2NoCmYEYe.ِˍ3\GZ?^`*0C<_N۲x}huhtcZ}O4Ӕ~> Y{t}hhF{W󹞼,G?H{(4_Y/sunFl \8uzFORL^:~qb2quNJ>/t6_{χCm|=`F>ܮFa+D,F6/a-{OCilb/7_ecosYC6joeO_'1=?JxY{̑Wh?2Rln[l*5Fˮp_f~46Ġ. i}ޗ4D̽?Z6_`njVTX=5S>h&MBםKI"t}p6OͫT_S4Wqb_"zjB`"a:ؤXvc~xp'L3`D4Xհy~mnkr@>ӭ|wR:&J;H^p*)#K#s2ʸqGDp$,?,[ɔ'xd 6m.?ȸ.hiv ;=$*FF^oPKqEW1 Querying.gdb/a00000004.gdbtablxUT \T\Tux ā E/3EùQcNw^MRVש׵w3'O6PKqE}LrpQuerying.gdb/a00000004.spxUT \T\Tux QAFQ oan.a*x,)Zkv/GmsSkvSUJWZ^^^^^JW*]k#{#{#{#{#{*]t쥫tݮozz A'3$Sm~Td߷PKqE>!Querying.gdb/a00000005.gdbindexesUT \T\Tux m] AFؔ+%$uׯ3&vg3>4}`( rVdυ#^,`,Uqͤ][gT\=)tMR2əZj>MKW|{"q X%6MPKqEAQuerying.gdb/a00000005.gdbtableUT \T\Tux cf```I@ Ğ 0Bk@ y(ϐĐŐʐP p1U1d2qDK(PPq2%`mI @H-+WP0( `dae8*d7Eoq{Yb,cF~V:ϟ} 2}u9_wq. _)<Jxm,wkJtsIV u~:e8 RYzeo#_D7hZTCCd;Ӣ7UPKqE:() Querying.gdb/a00000005.gdbtablxUT \T\Tux  =Dl3#Iuy=|^PKqE$5Querying.gdb/a00000006.CatRelTypesByBackwardLabel.atxUT \T\Tux N0}ƛ_16>C%CL$2ʹEo' uFuES uJ-kꂺckxG>F5=GEZL-.lRؼv)T3[ gD!=j9Y.O# zHev{ >%cVRXJqvYƞc9^yVo/&};D>èۉ ޱ?+LZv:ZۧVR|ѢE-Q _PKqE|6Querying.gdb/a00000006.CatRelTypesByDestItemTypeID.atxUT \T\Tux jE! E}O]ο7} :B]@d@m۞G9w^7޺{p{j[$uH9%HM 4Fj#b/J(X$T::RG[mSnJ(d5 6(2ή6| cqx=0)$4I18b| զXh3n[EASJ\jLM*怜 8XTH" 1L~9 W#`Vɦ--bX,W/PKqETU_4Querying.gdb/a00000006.CatRelTypesByForwardLabel.atxUT \T\Tux Mn0B[~໴E. T.zqCeؖI)hX-qpqw'{:WD[潜ވ}Wm}m6c yF8;{׫o|N| x[™8ϫ~mkل֚REGpi4gFg3,Ԯ%7R}+^b4{f9wU̥u1毦˰ ["mv`M7ϒͲiaT|6{ƴt+hޞv7~;ǗƋD%Jՠ{PKqE/T8Querying.gdb/a00000006.CatRelTypesByOriginItemTypeID.atxUT \T\Tux Ɋ1 @{N|En[%e[}e -~zT A|\.?/x~Kk6}JӏM!㊶u j cQ[m`ű \Kd*egsUZy'p Hyk{ta.RaţbÒ;`͹ .t )56<hD2ǽ*bgPTDz5(jS8fڨѽR}ʇ%GꐛAGJ,}-GՍƀ%*[NOTxeu)چ&Ӂ2UM/V;p8_~>?:߯_PKqE,Querying.gdb/a00000006.CatRelTypesByUUID.atxUT \T\Tux XK- <<*[%c6jH>ja5iw.$kFY}2b!kvmu'("gwsT9&TFk(a ]EYRU|㞥"؜@'.2C-8z6"".PIB&l%kv֐p?c<{ۢRl{~ƙpKG=T{Pl2{ Lu/\*qݰ .\p… ]x_'PKqEdZ!Querying.gdb/a00000006.gdbindexesUT \T\Tux ] @EOCE=VgL #-+L~]WȰr:{̺[R س!$a,:RVnH4bL9w.89W%{:5>1;I|"?zm\%3*iV>Y{7cL Z/o\ZQ&CKPKqEFɗQuerying.gdb/a00000006.gdbtableUT \T\Tux U LSW>Ղ'7pn:i)T <)=D\W,-d2 It č6dFlO64}{s{Ϲk@w ܿq#`~^H|G :85(k룥Z`2pȯr,kA\T82#' hi 23` 4д=rFLz(?5T}(Ċ)'xUmCPWidv]Ek1mv}.z_keet/މ9L) Lp3q 7,服|g8ROZp&8c7@O?5z)%#拼cozp ŮL_1'fڣ5p \k&,fR x9°İE1ڈ=9 p ԌnlL|73m-6nWTs*f\{%j3yM a*BaYVMLJ#_Ey\- 8bNϧj,-øY;-{-~VvKͼB*Ø~8ⳘI@%] T ,0ʗ)W\wM77> NmyA_~v$Cڐ#iJk|\41OMڢP:ewS>y$U"$T7/Um6r2*s!Lk TfKt%PmWPdɶG?}'Y= 1~">hb̵G_;ކw} Oכ8x1rc 3kN>C} iBFk P,GR. GLW6(N|B\w&Xxg/@WfrGC4e=?yOl^`4qAۥe&2{^tg zkyel$*R#4=Jv81犝]PKqE銊S Querying.gdb/a00000006.gdbtablxUT \T\Tux ʻ @D]&إ hdZ X=0BZ*;]lITF?/P+ev.gjPKqEJN0-Querying.gdb/a00000007.CatItemTypesByName.atxUT \T\Tux JPu-"֪Ģil15%I}LNPz#J s* Ru~J1}M Q;"ڙ=)Gag_D-pϜ}] ͙JKQ>qq @1bJI11!%BW6Ӑgp,r]̐|^>.dҙСk\ [QٷG*B<'Y+5i<|Єv,S}4=UuuxކWoF zb(r}v1gvXl[Y|W`M1ɈlfwJRT*?O;PKqEöy5Querying.gdb/a00000007.CatItemTypesByParentTypeID.atxUT \T\Tux j0awy ,TɻM.B7mA?|Ƅ1a'X5؄m؁#88s+;x\\g .`na`v ao{5gW'5&TPI ĭb}W?]Q8Vʍ&I(;J،֑;I4Zrm&z/aUQmó,%̙lĉLrG݇Ff&NS!+jRqB"ޜFJ&*Mʨ~ֿ{^v/W 0?PKqE+]{2-Querying.gdb/a00000007.CatItemTypesByUUID.atxUT \T\Tux XI+7 <ʎ$R$R$!&PFî'UEϏxx | |/3wS}W7]s#C G=m{-+]W=a2=d)ID[Uq^OKKAwx&Y(\G[$EfK.{g'x#+mҙ}eQds ;QUseF{ţ6m,x+$XMwX}tĬBbe =vXvL$:rsIh $-)k-ĩpb);y 71@ely#_7]wu]wu?7z PKqEݦr(!Querying.gdb/a00000007.gdbindexesUT \T\Tux ca``b7x NbbHeHf(a10@1兀ؙ!/20T2Y N@V(BT21$ FK0!(Wea2&[!7rw3-KRKsuZ:V=dݵ;F}[~4ocIX%C&'(1/=U%?713UҷYg9X|ɱu]_a TO_nf~bV,t~SDR RSJK2sA! tK *E#R^Ǥ⒢䒐ĤT5=lgV$}vg* ` 1~ju ?݋/7&ݯЄc #m֣gc~/ѽ-䜟S U+ڼ=w{pO 0zk(wh5R( k@$&_Ӓ{' M.H~HK W䓜mgFFw0ɪ^[k,7/eD/gEqN+<%7gB,^ĒҢTHeT5gَ\N^Ȉn2_piQYj%KMR{,WaS4z_rPaY[U'^t?vt9B _iT\mޝR]X1]X %y\i[J_[ ;78DZfUa)-Э>L/@J-/-JNYYX\NS'"?bjӯŭȥAf's $Pg7,>?wѽtP[b墇ٶE2'oJcp5ÄRsKqFf${ x2 02<`PKqED} Querying.gdb/a00000007.gdbtablxUT \T\Tux ͡ `Eῴ AB:GBa WWM0ڎw60AYW/l0VBEу 8n!.~@-Ɵ/-ƿ'p 8g=:=a7xx64 gIħ`0Q>Ķ]2އYux~֗Ov/MЗ klrV{*'2GB5N=tw|^^`NJʱV/W]rj,:ip ,LTq G7Bp75p"p(ٔ>7OEhx(x8T|%hg'RW K`2fS8#ie7W0P]O貒ֳkGbX:#spcÐCKDTO냉~`^ Qޮ޴),M̜q]|e"fsW؝6Y 5mezɀ+ɕ=Z88687X3u0c[b?y~KN&#I+B[)7ϞG`7F#@QЫȑ " `jYq ^me.<7gHz+/U`wA0 @<.d7]8{ e8FCy~tlr7*wnWnKoݾdzFs3h笡`bLal̵N{ۭ eO3;r|XJsO`xS$H "Kt;(z9t!4˨Լ͒z(r"e:H8ZZs0VzIG<pddh2vN@OzRP1Hs|=8fi/BPWq wW4ˉwmpBU"bgV -sӬ ;kY@0VeQBDV]ޜh{_L!UZ^O%DŽt hyڜ'kZ/}Torwٍ]y鬦CiDyve%3e7PA7IAAr;&~`7 a L>7+p}$0wt֌Qf#aҼUщ!`iZ!Z IgQ-~lW4&;.17 a}MRF6`QEI&}>IG+o׃2lq:*) +SG! 3X+Hj765KxNIvy/VUo-=X\}2=+gbb{-a~m4T'Y@We饕$4JXɔ EO͡kU%5vu X|h;ل-`kOn;@ _jI>KH?:8 L$Ua[ܖs+F싘4s`89K9dAsZ@kIi;l,,dT 㘙tx/D_s ZIah,.Ml5=cq񧕨 ]S=lMLOkU>v5#W?&B0l6ҒFZ:3Ԯ@(4gbuHlc'J.+atC_zX[ͥODd *ecTpG >!J TWbu\ᵴIQo%T[M=(JxWOCB ko 8#⹳?m/b& Q iH[ `ae} !T38k%p.4빧<8Ҳr‘p=WN2BwD5up" *Jm9iB" vuVsyqG;׉x(C \w*XtJZ*^Fmʹ]t֖)ֆvO06Jv'o?P{Ypy3N7in$y7mo>qTvOJԒ2@JvQ&QFc0aLnlWq[2w;Vmg2É˕+ՂJ7J+`Qr0Vf"@\7AO ^ɡD0ݨ$ȝTm`[m2kx`TȊH<{瑻T62K/IL8BRcIeZm=L-Gr6=5fZV2lB΅Bf"[[-GcOMt$+o3I6P*} <ǮmS;şSbhÅ(#o4#:KaG}啭F&"|ZXd@,io\2ʹ[&y!sbfqcH&s.g ? 6PACԹ k5P/$U L=yY2u!ĘŷmȚZ$h `:I"5^.tmD'k?dLӡ^sb202IJEX4T=-bF{)U1,>;!i /,'cۇksZm/lZgko{;T|IUǒT\tH_K̹h8}/M)1y8bbӕH 55o09`haZKjhorju6CmB %_-v%Ipl<9kL sR` I)ѽIQ3ZstDWa2O㸟y½[m5y)݃MPK]rE1}{1M[R-F#˅MǠ+!\ɡ̺XEѱ𕞚_?ϨT=FGimSd\mFx7tD`p-ľrr{s9ԇ|L1YwmM?_]B05~-]y'p I{& 3}sh} rÐibש3@)#VeВ!bĻWw9W׭ef!b{MISX#x|@˜u) м_Mzo-ޡt \X?ܙ|ڙ'͛ёհ<@cà?'?x)w'wz<)ȶU$lT\g|})`tIBmKN6E'r:#/H.;.ڮ#j{3=tkޯ҆|| ci pdoo~ʊ+f7ۖ`km=;=l/"ZZ.yVOLѫ`kUO`m=aCXN~:NOC]KPzQ<9v.GK m #S+bmx^a \Mr<RG_gSjދFuʺ7%I PKqEQr Querying.gdb/a00000009.gdbtablxUT \T\Tux +qfay:p$RB\$#[<\XR"#r[{ƛL&!(/B )xwIB'! Po&aez`,44<6aV)B* *܀.<Ԧ /. 9VC*'!4,! 57ԹQ؄W(t g%3[%k{EW.'!3O ܃#_@  ؃(TrXG(-`[̆`S"6 s<1Oh3ӚUCr<)Mnq\:%,nNs3'8I8OR$`}mm6A4P.RֳAQ~WqJ5i٨ALsEZU>leLVqӟ ZLL1 +j4fu?4kRР%Gۿm| ɓI&d))~?PKqE{+B!Querying.gdb/a0000000a.gdbindexesUT \T\Tux cd``b7x vbbpepfa10@1PKqE^4l> Querying.gdb/a0000000a.gdbtableUT \T\Tux ]Gw;M&M4kk/ҫ݄> 1ޝ1HH(BE"R?N* !Yx+BP @)g^}&:%g(B. ?cgtO ʮZE@5#4IWZC h BjA ~ـG(}.|0^A @ F>NlqxA!頗 {?f\* N.Mxnص1Zɪ; |N =• B+&і-9O;m;/N1Se|z|2B37M7LW,śҷ 4Q۴lc]أ%פu^C}H^R5_J\44ÏBЄ݅3|6f+wK9 +_NK ":Jb`_:"AF:&<&%ϔ1b6z½ۢJ=-ɵJ^' B#HdAK) ;iodF %QQJDK"}4~.Zx8V{ǥ-+=*s-aP`?x z{My^! Z#ubK~?ĝ=YJ =\I"BGZͮQLނ;Y>ewzm q:^ V3I?:` Nbv+ H!z:O,;6qZ> ԉ䃓`9-a)@"-Fd?'-p;vs}.V~(Z[Z ?<='B-lےAHyMU>n_z$qA 6@ Vx$>˅ `K vtS.KX?^΢hBVcЇ_ra.D#N^ytJ|YnDq ODXŮ!cֻ\} $Qµs7^rD U{O({mW(ynuGc!'YY0'0M;i'E?}%?o\?kLf]|cG\DyH%?|GuikexoxW3B\߻up{WW0W?{GNP/ xLg7Eul.5k~qvWsZ%Oo4^aHG^!YX^3&~DmuzIݬ;49Q,kߏ33 i%9 ϻ츁/W̱#+JϿ&YrP*<3rK;Fonnv4X0 bDh{O(gxp!W*k<A8 YXȲafve͗,o bBzFrt=֊g]EMOײS뮧 "Q`LP*0I=}L?xpy39دĘOȱ<&=guH:T!z?ܫAA,L%;3k&y<1}DP::okOzNgQL ܻŚYnS"W`H`Nn/3&Ƞyd|0wA-ɋYbKhb!˱8%bgA҂-Ox߻R^]1nY&ML+ ~ YsF\rnr88-Hoi8;"U] s,3^mh32^~V!uX]a1>zrB{pt2I:zimĻ>yul;SlH}Jf`uXzNfuAfۚ{+C hlZmH۹"&] _T2UA?8*r7ԒԞӉx\J:T7! +5|q-?r& *?.+dV2;i9]2hL9 ڌcF_} |{'vxf*w@:kbi}w /LwEc5+XՅ?y8 /Bf_R(.*6Fl=fNb[؋S/WArւXrld&DؾtFfz,]eO'{Η_Ԇױ;3**=Pfds*.)Vǥs&vBj~с17Js[{>y*5kvT0#X?)(lz"0u7Ϙ~GbCB゚oHE| mb@>T&L 2_Y?1ϲkjԲW",c @Bߞ?THӇk+q0i"l̘I2rR$ړ3p5S51#k'c [#CvRSDn{L3vLgg].3x4EdhD;F0ۜV& }s;dz-4_ C"pv²%W6I|{ͨS1[n9* O +0Ycc a4ԟ>Vjcoxy\>Gv2*X+xLHi}oьH:n/ksd;Ů5ެXwW)sTYټ׬lJ3k!!k}%sk4^kuwh;Jbk! |U@LL|QI^zʘ+ˣ Q}ƒ T=^4\MbA^zq> &pN^P*P}-=-JOM`'5__Yu~+Qw@sb1h>1@ݺɿK=ً^3%+XK8#xJ~V&#S5Xơcq}2kךI/;Ț#xՓ5_~J]n>֒/" ia(؄ȠctVD"i1əi"!O`OfouDm sfb^mb))~K>-OV1IIu1ZP⒩)Di`O;dnMF.U'k:S15yF>(cw=]?{_iP;)x SrԘ7-1V3 o}kfuT\Mm? ;:x>VRvx)J NqKq`l1?j=Sjٙc/a<4 %L@ޞwjj2l6n>].G&C7`N!Ԏc|,e5}ãkyx^[Z}hQuKmZ!} ,(N3,z?S+C3춤I&l'/`?ҔwD3sM_*_ e>.|8ȇe&e`N~Ng^6 Բ۫d݁N;/7ޞ yQٍ1YjzWe{>,5̎]:A.5Y[ hXjjjUWV>b׊}o<>m]xKQv;39q鬩 :zQ9k|iFf^ޯS[ Ӝbevq.G X|,=/wP^4ujCZ0w}4LxtyL˱'?sN7yDZ0`E(22̣^2p.XjL\)aae7>œ-Kr&iɈVnֻ=]Z}\0G8ҁ 1`wa_(uY$.Q*U2E ƍҩ܍^v@f_&oS/`}xGd&,Y[g9ξ krJVjR0mD~@g.pȱ #?/U9g4`?FvHt`44:n|R6,W>.Ż˄+S)3nsӋl/' K1P*"\ĆD'j^z3߽v~D6l7V+|ktL%;ք@}N- :zJ;+g.8F9!vm&l=X9B89BqkP<_ |>b4H$y ԊZA*jӆAA=/K*Ɩ O־OuJ2p{V\NBq4{,ֿ3\CwvY3qc'ltba27#=`\!6 ߟ(GBd>0hX;à/nك7R1;]'v M9&{aӹ>GCuY/ 3+i_e$F\~C/MgExX{3?}iRx'Z)ldKChb3ϸsڬ敥Zvxi Ycm"QhMM3ťujF Zvs֭16Ze\"x9TݻytX9ZVDVx.B(TE3=A9꛷YxÚ %V/le͵z79'8X!KhIO̹P,Ztx Ff,̄V{Umٛ]kˋ 9GɃNHjL+\M؀JYǚ{8;T)}ϧX@:x=Ǔ]P8auPJF f~5/3kّǫ98˵8$̎dhỴVW.ιQ\KbvB ^eJXx0;Qx~ܷʋ2;;[G{PK V7ܞO hjv啺wpػ픶656nYQ;֤f{PkG&F@ 3~uNeV:{~O$|.M BD,4{=^B8SC񜜖/^:SimڭfN&O-QE R2u1H "sSjE >q|W`~L%? |z#x-"N])7-j\\8/5Bd}G߅ptNN_^./YI7svdn`%rН亴;>P=!x64CX2zZ?%nD w^1070Pڵ,X[Qd@ cHzqUI/Z~~=Lʛo^l7;H "vskH h@{&H$b R#d/ańf夡EYv IGt`߾K# ]5>Xb] W)(3!3|ވ\Se5-bL*HD755jY\0D%}Ot+)8rČg.#Dy]X[Qڂg(8BO?SV >kq(&0s'(51rB&1υ|pkzx@!v\jɀO#mHkyR{DSJ @C]pګs<Z. қh1ob9?MXl f{Up`D=&'~OkYeoMw'esHyxOUzg#r{rI]s;O(FjgHh1/Ĵn}>Pnu bGv+M* 5:;:@,ZP4z$e2%D&z2?GfEZlBc#SB ?`kZ{.`|rpnqϻ$xT 8d!~-1m˒D)2G_$@RGC+Hh X:?-~9~Qb;86uo(V:bmfʝ mPO0^鴖XJ,V1Czh+&T;kyN qb\˜ k+pAvQʜ ъ:F+^T`ELօ+Z1 ]okϕuR~mcX<ݏ]*JB7H=IgtTP&oBآXK̎Hɱ(/sf<2,=սGۆ֨ՙ?WPa8L@ kLڵy:Mv*[3z"xWҙ,wf#o-vK]wCw+/OjRb9{M*GmZQRCbsby#D ܏9wkaڻq,յB8$JI}d<\'tq6s96y9b~Bjyc^C<=9h11=x!oy;hm1]ޣ{,&T? @q 0`k5Vd1Q}^f7'7 ,Jȣ'd=n6DŽ[OnkͷXMȗ?xEhl\?-HŖ8Tؒ-gTJ^-] x=T-?ŦZo Ř$bZzNbպ;+Fhy=ٽX ߁645Cr"**U5Z}D;w }ot {#__A bH't~f=15G$G ?Tn%zԷ2BPy9B/ukH G ~:1轞ʟLխkdGEXvM 1`1'Ҫ!4+1Zu% {X5Hqg\OEZm DE.O͑&Jl'L椉FsܒWUN|I[((٩Kd͠X=x\vo,GI3y%<( ź?GŲX) s9o| {&Jy c>wqǞYusg ,1%s {1F`S 1Ɂ(h)*j4d2I-ҥsz0ggkxeWpxC8 _ϻv q[.C wZh bior|.-OFV5Ա< DRIav6NϜgPOpm5pmc5)6_JyzDNTC&ofǕ8=oQ^_ G-ΩsG ț"3ʇ _)l E;{^a#_"BENK t}Ӳ7潌f*O嘟)Ut娍vw٦7h,,OO jzw~'.d(̺֒,i >hhKMfw,` O9%0P?1w@e 6fyyRw j"ueGI1Yb. j'0нwPFanBSrA,푩 A@m4}Z՗\iRנY0 X71e* : 8X6JA@E Rz"v(u<-#O7#& t+!Ϝ:btoĬ)n;;FHm?iY922 A˷$) n*3ų{ =yC(i%i9B؜hAs6WB ӮՀ{svuhxLoi\`4 .ivvĠ͍̓}22cxM~MJ1㨰'W<7k஺5*h=2~׍l>lK)i .c'ޒX_Ҵ1 HXON+/A.0@Vb]Utv#i=$N5 σӕֽi&PJznfcG~U]"Q%Xәϡ ڹ&|%c㉹'LNIE\%W~EGs/Q`i]:u=Pw 5ZFGn蒾&Wc5T[}FHВG.Kq@6WWۂz|-‹ QR^i"#-w۝E@gAzvlb~A_4887gw 3y҈ x;ޥģ@X~MH!uK3zģ=hw<LZZ1z^]LAO4wMCJ$h<]pGLWq<zM}۞ p԰E@b1Y O[ܸK<=J= 3%[X?yZ%fqxr-M{>j m dR{),,=Nhx~z۱h3vZs?0#d*C#O|FS'{%5p>BJ݀?38wnhQrz@SK͞&5x)8ԤqtIܟc΍)~\7|Jɽ.QJǯ<ΞD7xZ=4 W6L]݆&u;< \'1N_El6 OC삷N2Y ozsGjX;ubjVaEm#}TQ[,P#8(bHCm@NBF1@wS _^}'(c95T{6=+>>^ Fw 3>=W22̍bH>b%dc`>ְ͂F[4 pJׇ ^^H~XS:2*3Z3,lZNxG4}!f'o?\HAEC chFAw__Z%{z~_UbUJ{ tfu'ӟ-^\sqV\\[F̠ UB0֘ y{_v:p#OqOP`'uըe2 y>?I][2 wIR#{<511Rj׏PL X0Ȗ.ZiD+qS^uLeTS݃H{_U%p]On whmzCE˃جb3 t]X+SJ1;bPS˵` #8a0WmX}~Mh=z45k\U*7%9|%V*ٛ6iOBP^{p}`O#@L7ږޅ䕑2{;+Є~/h 'W,iMaԧ]GE%fkXLV'5y)(`$EH+ʷ~I?G^^W{n< &"O#`}}>N7ǏTF%͚er.Rp;_h;2RS^87LuB@ڏ LxnDk7 A<ᔄ\^2%@fp`Ϻ'}7#QN)M.rG]=.eT}@1#TT=yT`GGkO]o`FJ/׽kQT)s+Ϝ +)otfr)ErJ󪮜bזHw-s[RkjP[7ʉJ oKl|NVۋh铛a ȣy2Q $]1ON`>@(`zY1@rpHbyDjh-y#E,/A蝁S6[|aI#=jP_:>|Vo]"W9.ˌKt_1!dįQ*uk c1ĨqĨ)ϙ"tm= >Lw¯=%N7/p1{]6:ꪠkiGBRpw\L#3Qܤ2<~( tO@S1ʃ Cүd+-KsoW%Z۬ѿ|ޮv0坆n}[#Ψѐ[[}D8,4cA̮'m 9ΥD|09oj JxUa,ІaC<tL0[TQ* Dn\RI8NHO:a޾wY0:s5u/V)< rciq#Ͷu]}48j+p%1'J𼝒ڝ3`@Ny/q=q,R781o;f{}}w|ü}PglKޜ kH`\M:΁BiVӬT^ oqXF.jk B MPWk敏|f7&!q>)+UF1?Quh*}דzbWjcWդ%t=_WrRj_"]_|xMcLnzbYDn֥5&WUiNc *nghwcf+0OdPabyz3#(*z cQՁI" ^GQ@hzWѕu!3ة<66KZ1 P:.+z8 û C_Lj{σ:|Y-[lAMnr-jD DLyΧ5z1o~8"Ȁ= 1`+]61 > wI«ΤgֶQ>wh7:=d2#w{LNsxLojt{I#5 *_ n繮TUyy} س2p*Bptei|4P?Hk+sYCIՀvPTRvKg|B"OMM>s>)HC%m' ܳޝ]-.yʼnH7N K3 T.g{H!}&ն䣠 q¯5ag&rQJr' #ê+PuߜQB^3 !h윿ќ\[:_LB=7%TbZ6'sbPȋz fLո"TWri+27:]uen.\}!Us0YM `,V5}. LGAhMU{Rjgxhw#7x;T3>6|g O7{ gvM 8cZq./ôv=vop9q,vx1􋽀56}.=*^B&a' 6_I2?4nIF gmy沺 /=cC;{3@7dg; Nn4|IRuRS"<,=s!Qq*%jǧuzxm43RH1ܥj =P2 4+;O0/QZNtn^c︒tJ#TG7UOj H%lA&$Mazioj-}s 5&!n ZzIO0j1^zJ_WHjL-N(LpNϕhhƓIaZU*NI̢ݴԘ2+  *ςؙ((DǺ__\LI!ж;L z/#قI:"tThU<7Q#]œHaB`FޡR-qTyU )Y;o|z4v@+r`K𭹜]$^CE轍+#RCQ{`d b럪UC_ s y5W~O[]TrIKiԺ\ncT_jjBYr ԟ`BxR;';4 ik9q/?*p'z= wj/}SitBߺ|')WzD:$aER7H?Xpz@&<`iYӶt21}| |eM0qvPj}=λS՛N3ܹ5;(U($K9D7Iӧ^h>&;gIs9q)!=Xr6ay[qLosh.F 9G  l$xEot Ig齪 ,Yl5EޞMKC/"mN`+#pK!#:J{ rgo5; \V r۔[w y~O3x@%^ЉXꇚoi>PWe!;z:ĶcEZ =Uw1ϰxvfe|@"<%{$,6ۤ1>: N] hD 2@ 1Q 4л 3,cPóE@r\ChhxQ%G߶(vh`~*\:!M5jSj^cUsD4z^7t'c:KVi f'dROi8՘/Ji?哾~+Ol/t͚D`qbkOl dO- 4$Y qƤN0Hy-QPn)GNMItNhbnj4L5u%WN$NMyK{oiҊ>)z١I(4 ! Gf7*ip˩Ah9u `$Z$+j_UI+\q-OCcka HlNӱ ǫ} SQ̭M#'ϻ'eCʣ$mV1ڎz܍N)%CPRafn|>G9a^ZŁ$ |zxj4aHn\Y3yA0ܕM:)*g6obzKBH N'ՌW=: XBz tҝzz{&{4"Q)\\ơ9ٖۛ4&7Dq,w`I`uN;ő>]1]!sem6rz:J+k l7r֭j@\0Zжn 6=o/%~̔6[B\O\iqjs#  y%v2WM>ٺ?-՜;T2BË\~5('$fnw;{5 ?I[ P:G7?XzX>}%"[Md ԏ0N{j^7?zPP̓NǨNUW@Soxl Bo">+0'M!0Hg0D·h/)+. &Oȋ|[*Ɓ2]6$ t`j3}!b/'@pLdֺ YoL?LC%yY8v&&C$ћ;tZ[|*. VݻO.+P}PG.Ӛzpz#Ξ^{D<yv:t?CP7XTl4eD{BFBcHb?)3}h^pI)fogHXKHU#UAE8;9t0yGl{ Ot&,ǻ5#Q"u3Zw\ʌk4SCvF{'wu BM CeA%h(Ʌt#D#[Q!mJWV{kWj)bO#GLL//驞{g-1?h0.^##][w{!Le0;? %]JeŜk+Ch3iz;Y$,@=HҼ+3 ,н <{a.,uf`k EDG &K|>sNS={<рb7'[P%tR4Z4Lp浲k z҉f9im8{Lr'~q,voMC[tO؛q?^ItSU{_nsg׽@vh !%|5Zfڨr( ^t9z`&8 Ƅ:QŜ7 9LaXG髶qO?R12H/Pz^mgwPIϟ1YhFH̫YjDMΩgb2u}F CEkO`ur5q9:}>l滍ԣžσ萣[aXD'kPerYL%y I8p61}V~u/tU9lናz¢m h<# !vU)/v$%C HPw\: qzRSAGT3po=SNna*cPdZr 'R|񌭁{GL&^ՅƠޘbj-Fq(o]aLƋ4t+=Z%䕩W _ 󂟏ܬEmW+KbULeEv!_g2G]u(ըJ>;4yGih-8>N ~pj!.gŢ1>ГH•47&Iz- Q0o1:i3x!&;:IZGz/wdMvM(Uv﷏HW.if4_|L<[#r#;yĖ;HUv*Y[3ݽ* %z[h:!o>,0+͎9$S.q{qXH W+W3OfMW=﩯Lt c-Gܱ/3^Onڣʁ~'DhpAsm2P+dAwf?rm@e1O՜[0Hz=tL}VX) ykLUj4~i%O'q!k+jr @FS&=a+^11\E𓨘{iȯnVMbx\4a\0"yu_72\ i@N1s%7IL2ƶ4nքXY'O9P[W:XIf4#ibN`zc ?[=cz=y$~@H ~|л/ӐSRzSF0Z!0^|\ZQTC-@x|g EPϿ.+noHLC9^7&4U[ H`6i,351 cVXDm' C?Yej|E󸙑L2 kdؙ'9}f`gh*6(Y*=XHB kN\28=kurS=x=|>c&/DG(!%IqMw/I!Q!#&h D+36t6),!1~4eW߻;F)ga9<{I'Wh@[ C̻ԇ1L\ev_M Y/qt@c}y"f"χl?®<*+3iy`4i{ΜL?,=V&s3D왞󅔡BQ,I3=G)Md؋X,,"lEFeyU ߺ޻6Elio\ykćdl5%*?cUr\ufڎՕ̖Aeɓ-6<TuN7_Ӱ=W3PAl{t;2->(O濽l޸)|8Y] πDy؈&+ޅr78wt6RsdTɬ*18~@kA^!,Xϼڔ<xjěxZtoo{H a+ =JV ZΕPctK'POr_tV QLDlF.j|xv@6#MY?x<+l*![oV=A ڣAo|ؼ& ,fJ9O;grQfyx1㯴;]RWtŔv)- F,ҧOgAH@ސk)-KOCg/n39;aÂ?B]GfS& >0Ǭ*slhfQjjD)?4K0@cQڢP옜Pt:H ۉ\^o~v$e D޼x;vL/~W fԙ\^8kYB51+@oԆ*I,)v[n9ܹtfF u] h@n $؝ʉ5fX<ډ5󁎧 %-&zX*9R2$@w)>ͻ딋uhdsNvt {K?oMؔЭ,noћt&!g kmiݏ]nEh}֦b'?P߇%yysld֒c el0n`- -7$I B東1 t;eǠ?N'kRcD>"YnH孂x3V.iɫyXt=yF/[n'#eG4pA媪=*kÁWa[ I' e|\J.\0t&(Ş u$Tiu"v%m .%<3wi~_j):A1P"MtM΍b2Rt'77t#s MuO%v*;F`s)̦_f&=kEW6C9}]-ΪMC3BM]Ɔh /ކȝ~̱K«pJo%+ʨ|pVVc>+JPӎsr?r>0b+ux  SEh#^iN@tDejũ7˥gPNYGi#~+ *R )IN))DsaS퍬fܓ,h*P'@aEu-JFKՕySpT8d|!9(@޸O.NC" j讚 *$Ur_{he t=`db9=\}ǽ|N|r5 2w"<;$s= ¦æXu vg_'yЖ^Fjnw/r U6[1hnjn\؅ϳmFBe?~QtFyX2j/56"/ꤶYF2N7\ںVXmF/dSmS$"{ӂ>[`&rVV+2sG45m$reOާ[VjQĔ“:-?`ĺ3ZP =2.$Ǝrn+5@Y B}s/܍K}w 䑮fz"#Xv졓O% բո z>>'s_|s˓ҧn2+PxRT@r<ɸ+ogSlC+_O{qa"%GU<2? ?,spIڛP&#I$ co?ϯ (oz9.XOHɎEsYGX5~Pz}~{YT2<_C*NB(7決.5yIOTsք|XכFԅ' FfD~739ha(:f2*mH_n5@tT=cV h,NK]XdPۗ5|eW/?BS z j2nI $w]>/NǚQaVRBa5]ˁ5e_"D.|JC2v2s݇gV?tb;TB&( ,Ϭ@~fuS4OORW,#ci<;mmYo|\4y= w0%qMoNw~tK.{ 폝/ Z^}pqd,^7P_9@P_I(#Zsp4~nQ.wKWjY`#dArOF@ ]ҝ670c_ZȮ7k;3wv +*kѽЦbB GF4eOp%|!&\R#lKmOh 8؃k1sӉU()^LL('|}z~ >5DJ:>WeU0 $"$hޓ{cpr%]ǼqQx?,*REW1V!Y" va®eСD"|xX<c^&]vgkN_n ]}c>ZX.ѵoO 6+&d6V m2F_`Ѐg!vJ6Yl} i-YӞ峕&cx|qDwm2"LƧ kQjm%R዇||*Z.Ϝr)ԄiucY&`Lu<3|oLe-zJ{~uɨcjJXXTȵB!+PNc>Z[JOG=L>r]S_̊uĜ􉞳Y(iP  #S/ X zXˠIo߼3؀ c"͊ Nx:Zo0WiT3܀+t.<*j"OvE"OcWic61}'Qn{tٺAtwY4Ss5(?2E$Jbl=} pl8}TWt3h%/` $G{DqȺxtL41'_؈bz{V2zV؋V+/ЊIAς:Pĕ?{YS{ڣ[׊GGؕGEyey I̙39̙'j/2t$91QORHSS@l'3 "žT *;ȦH@),,VQew_UQ*[{}w^ 5:I^w_lN{K׽#N(, [kCT`.xDee-H?"eG{*C3<~`uK񍷱2T!cW6X̵$A4Ry["dOtmS̍XTT SXՐ'5eN+p-h2 Ҧ}(LxA x.X֞> dIYLQN(PJ8 Px?Uݒ?{~isKt*(S7p}9Z( Y x=5bŎu=i幐)'KO:3 4w6<쵞1%%p$CVu_Ӫ+󧵻F4?;|+v"~׎!o~㋿B0[-ޤ)0b1ui) u| v7M}^ol^)BF`^rzs^ZF%g+ qz;vJ"#:~)Ji z0 I */G159jv-hB,ٷڊō @$ M)m` uw% \lN͌?_ 5;G!@O5@SZ)8iKKDjm&n~/ΟjCgbW_J+@{RmaS.OSjP }NO}rzl~- (wxUfCԅbR^zO ܞٸ7VS521'@"8W(YrEn(Jhtyܙ.؊ʲC O=+? q=6O[x<䇴'Zٓàr6?v7(#t: y?##$cvȬ5M mkkB-= ;ψ=2ϫ0\8Y%cc_ 6Vb6>j¨4YŇ@5$k$%e-c=57:~E,=JJ{9&69 +9Afj3H,2x@}wU WKz5(.{6 s@"II g!Z`+vz40R*4wWzbK\`{p0HP{LEQBvKԣI:X.hU$% t OؠYեĜvՠKiБ*qKOzPKPv~ӜWiVJc5pvA3 +-9M(v԰#W]*s? c{X"?v#``+oOL2``;+{5/K<$<#bi~stvffvߞLRQ/͇\|Ɲ^iQȄ:0.hRc ${!-9p&>28bgc?SL&~,sγa&YodXd/Э0||< [‹0x5K# \H=+L/M|GG^=Xk T#V۩JAG uʞ!h !YWya#14?҆+6ny|tlcG`kx!(27u-(:ds}XM(3wtp6+%nu{*S=yb#r_!0,sEShzWW?#=$VM=YvqKڅׇ*Q^HAfuZ "g$r$ްl*;$n jM*|w/E$;O^0|WsŶʪtFzOBΘϸ֑ӋDHo Y(x|q6\G<_I 4#X}ѓ:86; ŽФH5T.~& gmB6{E?da&nL?B Uqrp^A|ėA:AfK40>ĮwfBZRO09gzȀi,g=oT)%}I3{2s3]Z1Ϊ)q{XXt Y)4gL-4DCS0uN}'>ެ^@_෴]N6W@,«x߳R7}wݕȊ&)buh,l~u |W:r;ҿ@?evݍ+*j[J*Y=!)RϏL )ुZ5|Y)0vxHzRCX -u]HEb.xpc-#H>~?wi,UZʢA~O\;RmƲ߁h^Ofh*Vf*$C|[  }L*ϰH_)QݪbN OQ_isX*=gc04WFE%jYں F c*s BW]駃S1ar%ZhMly>gO{K|\̖kjg]M|apRhH_qZߺU 3sWfvjOhMn@fc^FYXVA{V )^L|8h,Ws-iMD_Q FQwHOHOA,-,W5WsThxzMY7@\|au[&IX "NбTC,T޿K;FB1gN`wޥ4SeUX"mhyzy@H Y+z:CqGq%T n\pZD&#נe_Ib=I{ٷ֕hL}5yTk \O 6[M /`NOiy8Pӳ؞+ڟbp+,}Ǡ@:b*4,HaCOBv14=ShqeOoUy[}2axNNx ve~{ 7"tU,IJ y՘Ȯ"S֜tWTcMn$H|R"$Ocf=q"'M6Y:sY:MŐ8wNv20Jeݮ$jSfA#Fjdjh isZ;`{D| ĎndaK_mtM(QRV]9؋Ac#͹J\*t6"ūn5۩!"1t+o?p=!u܏(Ђ!l*i })W?-U&\_6(^< el_y}>ܴbF yls#Bo=GJfS(4W2px{ݱOprH Xf̹u5=hAѧu546S@(ح7uV4?N[[߱xͯ["Uwp!Ov4L2t3-+!*]V]zߧ'#,\0W%EFSn ^,!B7fkK-f6YW@n-Jȭr?N";isbSJ-SKD@{J{nT6 \N՘?%m`[ʚRG)Tr̭q@:ѷtϭD ^=u ^! N%d̢3׀Gn@r$q>~rCX!IH jomA!k+EpB1yҌndɠ4!"4\\ؤv9(# h˵[Zu^veUP@ 74ؿ&@='jgxg9ݡ@ H sril*?ca5~z)w)Ğ\}]c@k34Ł^ 0Sx\C\ Q3XjXUO|}!؂HW_4Em|wUN.eg6mhչ|z&D\9c 7x| ڥ]$822)5)ˇ Jb1BSB}`2z-S=9BɘC5<7V9` D!Ll{cu0{Yc#k38~vMG=V aY͍De{, V>+׆YK$v#v@Ie۞r)!pތڗQ/6< b>H~'TIrpanei@G w@D]d}lo)kaϚ1Al5M&xyTJlR30pwiðkH:f2 F9f3(6v\,u~(.^ N0:"_/.}=YcM]is36 w'4JQ_YzԓOU(?;\xLO&D4|hϸJ-H3. m7*RkPJAdQ6|-9y+[S2jXVs/c'&hlBj.wDhE iIH(css?/1.nx@jl!Xbv:˶ê4`ZvW&1$$k,3Ndbc f`8cs* #rSa!QQ0ZFtmEj"i`Ji}9kWԃjs*`)7 ݵ蛿lFh ;TJTZn h\7gnt.A-5?®<*+kODdLNwLɤ'֯tDIKډx2L*Z )4izh}-d_}XdUUeAyWO_;W߻ᄏnzws2C;K2fq]\/ 𸃲V<=iDžhO =r5 eX4 uHY߉foV+08*3_{N߆<}ꂃaƆ*{}~ [V4h3'?Ν O@wwׇw+zȠ:?($ yHw:S:vXkzCny\A,* p3z4'#DN_q~fU3 +hʅ7CV?ڬo4{+9cR<ȑ3)\&"g!0[4(_mеI - ˀcKs4w ~Yz$)vFZ6 '?Kn k@5\2( zlY?0O{@vfʝ+$ "U<+)ɖ'=-EwlՅ/BVUl.Džٝ[ޥSɽ/_4 'd/AF n-9X6GSe%oM!st4YF>QRD"@fA^KhsA}NqAϱ׷USMTN2fz*n{jKÓ Url|g-ܸe 3\%sFVǮV8]G^ }οt7~@;]}V*vIg]imo_ht هfwN aȊL tQ{~ZkoEvhPLe\`K *Bӎ_X7lnDR,LwY:@UΎUhorji@\rl9Yy ^ SdM 9YU%hj4 Dxڒ^"3Bᖏzדf dD9|ͱT0#[ӰgX E:]T:rgO' MMʊ/Jõ&/@䨭ً*Wd2-#"Y֢pZF,y ^P)YXBERuʋ;h!nHT.&XVFsK>X$*բ΀1o9;][ļH0!:90W!vD"Ӭ7G Dh6##F[+6>pI7)ʟ|C?y~Em|!?j2"@* # 8>=Zu]h=?7?e<>YA-e k37 u!Snam;Q>|Ԫ+ {` d%FwR`%>GDjPҧ2X|3ح|Є1n~4k#(j@l$X8:YBYt*(s m VCؐX'5pTtm,~]شx҆Z[onGK_3/Wΰ!^}0H[3Y]/UgHFĀ~rlĢCv.N=@d_Noc.< 鮷v ڃ?s{jBiF[>krQVf ΄hm{d'7VK3(4F^<wjg)b 㪼zV48eڛϭ"ҴE:|IZvœz?CaQC}H;,R!Ѕ eUy ǫ &7u+&m]NN' e0цSbt- b +X=/:yt3X2RDd$3fln gK ہLS03t s<3|M6MJD2qpOPi%y F~k!h{Ϋ VӇ~  #$zT#Kq7ߍre&lL(MmfӢflD#gG~ ?ed- n""A˥{Lg?je= 0_@Ȅ zX0Y[QG -- Сۃ|Vq钳-0.[&vrlALr@pzIyUBD0=heD_ D<%.}ߨ^5Qho-EsXڻRk ΟWRR))ӑiIf*{}]$0V'oY!7e9;"?ޕnOX,B'!qܦ__M)G\EnwՐ`,-"d_8ff-Ҧjnů>{4B `<%aܯpj12Xz08vOT( }Κ,6FY0uvyL N s=SCcAUD,d!egs,xp㚫H?}&0}|K O Ht`pWδicbC+ X-ݱҗ 0AsSb }yD.rpvW5wclblgTh~v,/53⭡o69'+=%9 a>?hk YßB,#3ӶndʹL䞘8Sj|%"dJIIɊ%(W/i.\!#b%3O_Q\!M=ԅ:2t[?_Xra[şo繥c}1$eWfUݯFk^^S(Ea4\v?džKƻ/Vb&$c6):̰%O jSؓ '˔FUƕ\5kҲf $[OHNvTqAޥĎ١h5QDLsTj} Z?9s@byI1Kt3eG}v O%xc#|0\Q}׼1>a&V-Rx/`_[<!ҕ{gmh<PbWa"0R3؀P}}<˘3zϤcVuH=Y@ |hcN v =@1/i+y&Z,W瓜P">T\], Yh|~=jXF*x)))3_ ڸQrm&\c X~P@c H!iCw[#JPG瑇VdCawKQ~^JK֢z<]u2ֽqGHV;Wlt4 Y={*({;3m[໐ЦCEݛ$#J,B0\JD|mTof\,ft'߲ \Ltu?[wFSԭ N72Aid{tIOŞiv4x"FrhXn-.kl+ZS}҈g No"g{i6o{ >JRkT2QMOkN4*x9c%#CzL,0 :i?V$5͎&^PX!/#Y<!p`dn}~m];Ҁ6R{҂ɹbA<߲Vm%LD99%U52kj1P'Q4%MW'a/݊Ea5-)eIgB̉up?u=1-_1@\tF&8*gjm"4s=hrs9N_^`e sϨ8vQQ]iY'39;ɫj&=Ml[c&dLә2"tڙ9l"A@Ev٭XTYDd}w߻oj\?VaAj`9*klYgAחC+m3Á71)3v>+WfLevv?AjƢKsrDČ*=h@TX@-9V)̼Wl7,3.j6XLOI;2B0[^tߍƕB4HiD+gY6Fwh Ւ',w~7.jH[V@63ďϹj CQbO8+_ 0G*_a-4LŠjZڰ?39tMY7ft?֗-*QSODᶽnܭM% :ߧHOJ|vJ.*>7G>\.|;z {TN3ˋ>t&Z׌/zW`m͢QO6:>îA3yO|`1)zՌ] -c|óP ս +d^n0$t3N[AQOTjn?A0? k Om?[oڹM;ӹNp:mz8Mb Xdx6=]_#xB-@TI UOz)-j_/c(}U=2O`:{|&vc/9iWܴ_4d_G1s B} YYh)V 1-ś7 -A[Krm.j7Kɰ*kBÚ:!6^5j*zE$l aQEk)*'UӈUwiQϗvNTPjT͠SN^z6BGu.swX&^^i M^C;{yk3K:X2I5}@6σ%M.Y.E,Hc*!Awc[$Rti6NVL\^g֔BLqKһ};D1R+XA~L+^ACfs,݌cP2E˚==? ȍx9HYhۇ2( KS碵\b]^n-*kX7`ڣT5ڟ3=*bt?tCK19X%yd`~Jۙ9;D@QX|>E7{n=3I L,a=.`X w>LK.Ŕw-Əř{` f)xH*lߦѱآTY׵ 2ىuqg7ĢI40qs`K˗"(11T3\%T @9A0#xP$pj/nPM\|v{w@GB[Oq jޢ] >СQ}d tğf{W(?`CR/^%!C;V-JDh!po! 'ͽjJ0t< B#AuLG54(gҷWKpD2f1b UEzoeyη>("3ݫ'֠'du83Bbj*ȸ.VPՙa?Ltx֔m}tKGZ ʻܗ1t|ưQ8CSmϠٽԳ=7 1#URlW:l֠`,?LJ#鬥Z)߁t?;GOY]I–21Hoj|}dtqu%hS $o6lޛ?N^wT}ltMX*:ӑ>16  ΄}h.n@--z X1X| ךTc+4g\XHUh6:dvcՅ Ak7: $2{(|;{4B&zEs}Q:NV(bKA ;t/|R U0XvAc. L_ҧw9r*X {'aH"^ >[}Bg  o(;F ]N;/uOy#g:4QD%r4$=|Dy9,Y )VFFrR;{npXSN[EG?K&Ϊ4(o8,_ٯ'uuhN ̶ORZms(,9Oےꣽ 3cV,~dBn z.K7z-p߷M'w@,XQk 2`I/.^i8קw$KEr|6F״Dqpwln'k>$^v-1/"KswD c zg!_mS)i8Z y 4GvBRrѓ!0\*~,L) ~7I: I f%n̾-1-㷞ݖgMFTeO{H й ^pи8l LT(+4fI$\?i;W0<3$^ք8!۞xW;w:PD|d.LA7lDiʾ 4n|QxX0;](<$w/_kD&QH`FWfՒ :fi9~'%>YV`#0Ra_Y"~]>FTq*(zݏmX&ڱ!䭧+~>i I),8uw 6Yx;j_wwSLV9wS'45H_vǿ{F&FMBcc;3֛#))1aĥV RyE^W :i}+ ;Td#29&<M)/w:CLF]d(n栰.?~ÃDk [Ho.1b!|2BfW ?ʹ7k^?ZBFc^5ZݺNQ]fVؒU"3cxYiW뚚XCZ"=+XH=6imQpuN *:g aȱ`u h)F;5@zXВ^B$Cή,B["}OH>N 9#:+e%@gPpOB5AL|l! cwe2&Jž4*+K2`b4dzIdM O~L-'13 `," " " ǦllJEP}%*}u{ᄏi!;A +.<ür!)r8qĪ4 ژ:8ѫ/$B"N嘕]'0o7зkPT`{8LqZ>[vHP13{%$Ed^{Z`45n"㤳J$5 \l7O Wb$C2rqS&'j Hlw6MF(3 ( nܷxE}`q\-6b0D:/c/vcA,Qb6OS:,N"ݍ |BGjST .Pۋ*ˋfޮ 'vqRtN`{'Wϴp8)/:ߢ⒞<-ˡ˗"Pwttܕ;Ѣ__c./{\: %utFN'7Y-#OT*%Aędp} IB VkQĭ\pE Uz,>ؿkbZZ舋bQ]] 0\An.ݭf/[aQrFGIt\D}*{ g{v5EN]@cs#4NB:JPI5؝!Lm>ؙ.SYiE;_:#3xtkWR03YM*okFnw4#y-&kE:1hq簗ʶF7&=:F,jЩ=*}|13o9C)_bK! Øt6>4E,}njL\u@?Gs(E9K$E9GW`!nJWpT[x |WofB*wlqX,9#gsGY/H>FIJ+x8ڞGtm4M~U;;N[ iמw>]oh ig1.-x ?Ј;ӤuUwebiYi4˙(]~Vՠμz*lySH"P\)'BקGɷ:5>|0,=]ao/iF[)aH֤`#۟cK:B_cl#~ARyitK?Qq59d z;OTgX!v}&*(v&)ōS3wNl5s[;qҍJ oYŝZx\J};JgxݕMhV=7/54 M2,-ftbXGAZ9BTXBHDA۝)KzBZL\Oa psf:)o703 f:l10 Vm!y#daA4/e)D`b| !t-z+e(71a..wK_>[4+\v +“y3_8J:^bu{02WTgs{IH'Fm ѽK9[z{?:`V\9#}ٱȻY`&?>ොH&_Xoy}9ɞ)3̞# d6Tz3my EZ$u^>P 'PW"̥d}͞>]R(|&w?1,PP!rْhh/bѣ m7V;2x[2< EkX3/y #c^v ;۳tҢoVOȢx:M$/}QYŃimZ,Ι<}ɷ0 J63}6VNg,.9/( Clp:?=N뗧h-cM,bMD؁VY4\J1~9ۜt#> 3nK<`ծtu|;3bŕ/܇Glj2vt%+}:wR4Hd̋C@/%ہ!F} ]k%9hK(dd|\Lzߚ/ Uu(;g8{GR~k  %&6 3~l\shvFC#K\dѿD7]#:p"%M-ގ"R!J$ `TV>A@[4 XȲAVǯ񚏨^6wTZN~ݻ77@w\ea-N4ƒzZ􈍌]dfAO |BiF, Aba@@Z h'!  W^TW+-7&EXvfvɤRм鐕R P+.ǃU'(˟sINamC9gDbQ_nVIj[l%1R1#B=0'͗צ&/y.wҗHdPZ1J(:乙Q 6-f苞v-QlFi/yn8R DUU&ܚZ==QPsOQ妈#Mϸ?vLOBFYQK-IZ%᭛ 3o2l5R'>%]cJAbjDdD܅)Coč VvFEk Z/0#{d ̚'cAJWsZ ۾ eg-3i~N}J΍*S+3(ݾ;BFP}ߢPYɓQA JD"mX< nğhs$o|N4boabGK zt9c V^}yDc{ZOXX8 E=sHȝIRk./Ш^\1D N%d0>75ӋV{c4C+zḧ|: gdXLBuOyЎm_ ?%ϦU'xd`f Uh%mg8c]\,C Q^򸩛yzr'@TQ@z;)ljujBΒbvgF !ZֹT&ޢH]\Y 2Wjbdbl^(lvp=n;ř1JPL|FZ,hgD}ꈗFh(ЋֻtTβoͣθ\40htDTtTٔ7J/?9Gr#fUrܥr;.~^*GsNODʋ%di2,.!wѣHA&7Gh;V/ zÆΐC ߫=ֵЎ<.]h!vggkIrֽPWXx/mÌws~0|ҷs v/Pi?--L_hGmSjUu1hS{|@aɕIN֨>tGv?Gݠ@7ZC3S #mĆO>tqxn< ZlSV+€u"1}|Rx + .MI!OgjTX%L55E9ѸTcň};Vv0Z},O 'گj/e':)8 :jQ߸P?A0 SEW9z4+(,3/ΦcGylGVX4z8y|Vt"6`sEdsp+:˺0j w':NklvS[Jvٹd'S^5X&Jm Ц8JAEPΦZnTnCE[Cn|_ۿg}NEFmU+ oiu]e ,>vS86vG3b& X } + ^cRڦGm&X<"@/o>$烻ϚгB"sJRBG?aOǍ.еa8U2ZU J$?w3=s3?FE @e}hFL+}:WKHz( Ie*v]]dy$]j1& H&@) J֗clÑ`;2T#M)vwTd 3}䱓zkj|F]ݏѵi3Ҍ7*L()!9uaV"8v i7B|]ɋ/DQxydYv^B-,WP&]}(H(14Vȏ)|NxSU'R𧴘 t~ 3'Z Ehv@?J/i+7Lesm=Q)&"~ѠEf6>.OگN>@lKGQD/WKS )v1a ㉟[/QǞǘu(MX09xmԛn  ym;2:/^CC+Fu::{`:`lH]|n iʲaa~Dyv^Mk6!m&=LX>f6ۭR5Ն*"%lCZ=L]YM*dG>XƵF\`NWӊpb)ۨA}S]#YCȏP8$bZk4݌(EG-Ԓ5E8RR搜 6t!c:Xsk6GG:Ml(ۑj!e@1u@9t!ftQc%fӾamnLh+`i,QV|)}+cs^GP*C}czx3X5^V)-SzJH~8{3Vvl;36X47η\WPqO*V%,TB8[~ GЁ lW|Xkq1XkB%oo&1|yL?$[z2QHZL玲K!ϣF\ Q S!܎]PFj@,b*-'n5%KZ=,"rU>^9z^}'{lZJvAUK) "@úG~6!r p#uzˆLu%|3b+<=`OfohyʐKk^[|fXB/S>hM5Ŝ,SB3 s JKj%E>5KxTZ,r]4NGgjԫ0g*\gjЦV G4}IFKuqXT: C#Xns= X,.JƧfv>HGMa.)9 }]LߪɱR:~2jNlv1h\YKEqiƼxr<a0:WI<_ Tn b9vMgO{RIy}sP8kU,qۋΓ$n3[V9btKIKz@Q/ nbG+ &Z)q.63Ijui;gsKM)hSv|\ۭQj7OLe-*'ՠڳcaVqg6Y6;c\ЭZ2U![}lp('>}2C/ٰ ut% >WIEFt.<8W̖qPbN!tNAbᲷCEђ#jI@A#MNr{w:%^,r!Vw"TȞHEEOBY,"l|J8K3Z{> z`!2t0K`! ??JE5<݈EbmۯT۪r6:~d\}lOŵ< zZw*̼|#_2\yKZ1]˖` o52{LL1M&srk5k!Asy-L^ Y ]7bM')r'ZO y&g$:~TyR6B@!biu}Yp˲B"%︉d3b3 -޽hUaՠh -pM׃S kECYSvZ;2L ~o12tffHL jW[w b;pjԁJތs~, yAmv9|‚-H?1o@lLtJD="Id.cɔ,E`3F3Ó5h*g}dg;͞ΓOp]?~NkB+Ɍ{9LvP"V(Hj 0رA/R.VF>ZH8@b/gwϜ9+ ;3755^D=,VvV;' HwJ#ZP͊Ңf2`#ۿPQ*-3{i"-J# Zm[ڕ)YK!ci0}.kn=l"D0oAgM.O"?r(Ԓ=f j *BDsZ~&I0X[~쐀De#LRe!wRzI(>F^l09mb'~1Eh\w 2=r_[4]pH=.}'oi*?rH\?{ǝ!CIu˗_TzIe8Ti#ۢ@?ܺ҈̉맞$b۞t{k>66qrqք6+Rzp@T+0{'}!ڰ?`ix~E !iX|] b/硐S/r;"םTc-*_+PJj2  64#8 ߧ״~qH2gjL.iL_uK%$Ӟٱj:ӵݶv;F<ugOy(/ O"Q[ w܏q~܍!)2SbJ*($ٲs?a2ZJCh9ia6"< ۮt2`Iv3ąۍ*Rx'+6,\>\aE_/pRVߤ UZQ `1G[I֬ }1?9433dMTX(SCFk _F70Kx=~Wj[8i"APPou` эPXOI '5Z'`4LٻU!iL`c]$R=1!Cr.`]UfarNUtW|C-J쪾 s\ 抬XvVx2`˔@ {jʪP^LLp`&lo1Nc|) H2-x٧ߧ)uEjl {qːAJ/9&_eƒ{lTd7+nNR}A=a8Zyb~z2G霿lW墐$D8Ƨ{Dj1H-^+㴊iGZNl2^LЬp%Rۍ;k=>h@_S"=`P'nґ\bA?.g c/hQg\qҗGl߯ Wy[4xs 04_xq%O+뉟h$ƉLc)ܤ;/ngoh)l1r#n P,RW #A-:۾J(! [0VT۩mmJG3Yjb]пrp=.,Lb$ےo}}Bs>ML!T,AGVM!- m. h5aQ1eh5%:78"L^'n]&,OETX[M߽ژŠ@bÁ:럘T^f~-joEpoJR} 3$[Tk5.h*f ۃڮq@FXh=SׇU,0}ͨdžJz&im,XRS7Oz!M؆s]9WO B/]u7=QƷgqjufbadPߝvRkmbZ/XEmUWCHH;ӶoQ Zz}(;ڛ2k9ሻ O$0;jӚ^j䳕Nj<ŎHUEv\ed5oBWt@ʋtim{1_߅d]ynopfPu˔PQ>~Jg /vÆe:R' O| Yl!c6f#tTyK=U ssw\h3mB.ݞ&{Syϵ: D."ǽ'$9/לmy i7w eH>R \vv iF0^q _ne(9!|ͯ7܅YٗLŀ82,AHOo;iCk>_狜?갗72KW??S9W1Fj#vcjc'+`VK_WݿҐc4GXJ-ş6&6-R!_cT7YD()$~UgXO=_JRF|@;R½8?1:jlgªUo>Kgl?:`F~L$Єm/i ǹTwUc˦-l5Y{_/bˋg/)fغl*-ԊE}_D`=ll' TP܈Ja9WoZ,y$3qiuIMP 2hDZNC|KNX@Q_5R+[u$,ZUw /GdT'd7ZV[|4b̢y_=(y| ͿV|%MT!mYzTuy!2ǹ; 7cjԠ^I|Fב) Qc 1N̎8Lº/FVF*1`ߦI[ޡE!`__&J),~byPڌ!iWK{F V.CCv-Q$?^ߐ[pЫή;78c5Jk% e' V3^+pCkHZ&P rw ludb^@J3AWtIWlqyw#u-c]$"+e#Ԅ)Z0|O-XvH|)%0Η(ۇF3bs|>h 3mY=tR}HHeL_LĈH-o>şzFSb,p2zlN|_Ӧ\Mis@\9Ҕ?M6}2ҏMʛæLb>=&,_Y 9t _/s2v[y(JW Œ^oW7RARd^2/{Ȫŷhը[*=;عtBbX>h׎#$]N\p9$*9AF< D}[kKt)|߾Ms߼K.^14aY3>!ow[|M^v/A[u,bi2# c*-M#Tv50 D8 ;$ڴ;?;3MѦKcPwSSWY=Vw?S|;@ˇ'OʼIH^xަ(ɿԒ\eQGc$ c{¨)xsŮ_#*É`/ b ՝FO@[m]iճ(5|0;:|pSK;C MWNޮ@3A|x oĞiHAu!ՀI$+ dF~/ 8ws^θ4E}) u}I; J[*rz73|{Ԡ+}Scye7BowWC2Ƅܱ47ll,nD)r."GеyL vT M&L!O3@T#%O28PKj}jP[}v}"q KEosklhVwӅ?[^nEu'2{bo8k&ohRWTFգpA$|wXEwDZ]JNjkaΧu̖VJb3gA]kS7{Lw>i@W} Iyn(KP RqQy~0BF"~r;>Be܋ (3JN&Yݽ4(}ᱫ%)|*\˼OB'TBCL>Bq6v)0Ul7lpLs-:KXԅq7I`. 'ӇS;Uj&|zH9<,NAMlI$-,Tghrpx*N܂$ƪlM:iuΓG DtEvb!u|Ov' ëCfɿ">JH24NՇ>u2>zrY=*ߢ|_@]ʭ'ɃTxhdjSU&3d&ItlMGȂHrJ#݀4 ! (% u{4Lw‹>Ԟ,%vWFuxy]y.NK[rajIt[ %hh>8q1#l#Sƌa.`S*^%芢&Θ;>728_co#.Uҍ b347PQUbB(Iq1|+Y`AxpסqXK{@8>֑>cfy1bJL[V4j+z# ]B)*(o hk ]WJs5AEH0f.)Bgc=z9chRfٓ ugjQf:H;JO;?_?6 n }f{(jymJC 9t [VV!zמff ?/\|("zk׺/ȸOE7gUWR6JZW05H(([ +U!uO +LήRwnGYʣCK8u׫¨&z9>,˾1iG/ЌZn("ٕ{= )d;HßƺM4eYOPUW Arsf1c칛x9:YlDJBq'n6D͋YۄUBphkkF/0̱bʩ_h1sAE1Yms0s92v[4~kW3Cz-N^yX@tQHX)v<*uك!H8? wcRG` 9vbr$&jSNӤukzs`֨ɼ=mnb> [k``Y>. ]9=zW`;Eg?}hvf47I΍-_͚ ya 7~D>~De:m/GE/*A &Qu6K-.[F4W.>=Z@(qTxghgvX72t%0z/tH3䡰x?LiB:Iq".6pAzJB29EB~' tYSd,QtaB{bUԋUH=HN/9;Y&2NY&nRE'#U}*U]N%E%"'̨Y vf{5(/0A$Dty殯 zΝtva޼"Pq`~Ncx8T,`}Fl7)]C-iN9ۀbSVjYPd_qh)W֎jPrky2R TUx05 %ѭI;/v690g=oW#PƇqpxB ;n:]_z*O cvwzscPJrP)f2p\p~VV}.Fc"}Y"zU1N> ؋ u*F,70'}R~> +jSa yF][fЂS4G)BS+/ X=u!] G$8aԳ-k%C úy[N$쳿ʀ;m^lvh|1F111V>{ L)=[7V:?OtU>tՓ6>_k͞Ɩ@XlO,IOWGӯugqFG1%B1 `>Ox'z=Ui@}3?:uԦS$V&"&oWGS̄tuW ԍiõȐ}e"EVȉ̻FTZ@JJa: פ*sE|HvG{z#70vy-)ՁUhVϺDb :"yJB vm=0;zytzL2 -^2\9;IÓn2FY,٤_T7]iԢˊ(I8],UUi_V MTE)s"b:2UT3dRht3|# k%Qf.K Ԇ WGteRT!]Pfqgb6ikϟ@˧ ǘU Ƅ Q:#=a%$=-ON ݥPm`{28?^L;.^ȑnme,n#>ȥᇐE6(jszNj_uFn&*o /hW@-$>bQR~-l+q XXafIJd(hq3:&xⷘPqs* K? b+y ٠C4Yv _tkzQښ"rDǗ%e} nkPpd_~ Uǽ~K)BlzP_]wUH=wo`%<#. siQPhx.:bdצh =& a!~@tQL,0 %MtpZ.Ҡ=k :}Ӷ'7 a٫=r0i9HNB  n2Mg 4~ H `-7#?adV s"Pdl$c 8tTjŰ*(-ne Vi&.^Mעks/cnN4af48}S K4^ B!OJ"m-Y1I㡣gx>%Gžz< 'L*9úQߧO-'a 8@,G KyyLo n,19ÞN_ʯаdZď&('GW1yln=Z]mx2AŅ#cp)D?.u2æ. p O'i>B~- gUFganOS3;-Z\ ,}'UW:ҹj! ];K`ev.e FVD2C_;þ@?53O3^J_ܙɤ|0=_${즍ʳͫZ_쫅e~e*McknߑhM,nf*>E C@h0Ag)+/m^ElRi>~s /P+Z6U?h"h+)ԲqӒ]x&-*k O΄@obY8Vo4.W0aX] ˗ `8.5GgREiM9RzV8tif+T)\wuUW 1)~epqֽ2)2×Idz~*m^˻P<,nOs!s wcPHnH-` EbEy`[/99Q hޖ׮܉@ce7;| w~gKofv߽x?j8ć!샃2>B(:ݙB54wSEiەU܆A^\~C}aT8qIE Xg+ќD &: +pM,R""_h-:T5I6S 8ؘiv ct\rZ\J)-DtXnht'n9e΄^X_ӺwCޒ>i&Z 8q֊qѕV`/tEdF XREXF=P@JYށ/hCI;vNRBJބ.qgO$y1`)6b?l tn W$Au+W{q,,t\(>= ע&ԊsB^ב|:5yA|zj ݔ(пtn<9Nדџ2o[5n9Z u8ӑI-5v^lf6[R1_G5YjBL-V >u88%Aj O'cÛ[0Í]md?7L eh!na qmP'쌀-ҕ[w,qζ =P. cfZ!3745o=>D^3`x2dc:=6~KgI++Fj|]m-nUDJ?%9=b, DǢeFd\ 3Z`4E@WW>zeJC 60VZRexnelY+Õ{q$#x>V z{x{IO,A}'TWUCI\ <;XWN3{=ɏ:EmּUd1z~ \TDd,@/6Ь>Jj&l^#"x"Odv'[Ytm HRs$I-;nƲkDo5x7": _8oNҪxF -,FWA\HRX0 ?g5mg稂Iz:p@=?B7ą$UVVΊB>T׿uWaq~>?ak|uDuoSeKo5rz I0X {+ۈ#^҂T ZSFNDycTIQ0) K; xoq:0*?>;4Bn&$|h0aᳶȎ_ ԜyKéQ 2!2Ovk3ZﵨPHnzGc:̮9HXGh䲮vtj#l:frH}*UdKż'fpelSy]H,taf|@mK\XA#DTWLr;v$C'`fSO*֠4 r VkFTO E+e0)CgHIY$>- | f(o vi'.+ٴgiZԾHk. Iˎ}c2ϽnPjCO-4#5 M#O! c K}fKL+|;ob=8S3; [-; tҎɿ&6^>'eTEs=7FD\[Rd@3:sImMwFUspdt|V-`u7'h%ߧ/IM3}%Kh7 [*v#i$qѱ/b!5Ug\Du$|@c6Ws݊^^F Mεu(@WRm"a$:}h$x\1Vs^9իDFN^JKXCL=~HpPw=&B5&t^ DLWQ9i߶OOin^s=&͌'v>!f;P-ݘ[Nqf;;1i߹۱<*λ{DŽ8|cH <ז[Ř_sB ~ 93 )?nu_Faݛ%a^˄J*I7;_9/]pq:⎕u>|h0mxv1**NO=| ,m5Iʖ WrO7Hb][9>jSni!Fjw`o]Do􇽦p ߤ/ !6Ȉx.;aIf!]aN8sUSTMu"{O;&>&:ځFNj2سPz->1h";:aQ fu\Hc/EU=M?zR z4Syr/ۇ 0s'|ZcrYW֡q& kKH\r۟OhVOt1!+s׋J2- a"č7 H_he"W*eBX'8>>G|4PPVDĶ=Jcm7 ?C^]3t(ɒB Y.C?kn\\"^K4H"ȊK_^..4ҐoBq#{9`SEǿ޲P-d6r >Qiez4 @x?]@@`e$^ ,WɎ ̉ DuZ*s"rcMogԝ~uC> K5C y?v1V1Ϳk&êPtB+C21/%Hdq)Yk(@~Nwʀ^`'u}~”ALU̬z.mfͳ-&v|篤nRoB73y̭6-&Me/L[  yl'{< BD-[GL_2POO0"&m!'#gSOGu]-@Gq3%cvf5&س邀zTe%j/Lqƾ _גPGzv.[IX, q?-;? 44p/c`L2A.r?Ȍ] ,:޳d.GkZe,.s|0TK_G}S &wJ@L?1Fnޜ_Tg[CQߠ[hऎ_쇤Dd{$C<oqYYoihL~b0׶’zaX6PR_OOjJIix"12Qcݼ cIs\ecp/ r|L4kr>HFX)-=iXLj>]SiK7c<+A c\(3uk9Չ Af'W}*{癦Қ5কYui9kZS cugȊdr or2,d6<-͘P4-neC  */dq`!f07j+eihW8 a.uv %T3~'ڬ[ɜMm08K-/&TK{e̡+7ҒW8 V $ i!uq:88ͱcPWv%E(2@vqer  cRVM5S[3ki~omjcԄh⚇neZNbtX!o4Dn 4Pvr.w{" I9ӈ~5xEH:=jy(l-*BBXchH(wLՓu:jt"1b"m2E`G]AS^񽥙޽cI#X$fw *~ Te{&騩 /a_-\2Pd^U4T|)[- ahP1Nlx$%j=wÛySIj?!>BvH, r_W' >ZʙO+g$<#x%8̋o1)FU';j =ˤFds.@[Wa7+ PMONj΄.vIIE^;8kc{v d\S}-s9Fđ$3|t؅ zjfDA_\̠&,7f[b!RE=H@{1>̕v1HRP:/s!f;>|Ju%{3UeT WIg<%m/QJIE.HPnq"9bCGJB6Xjo)RH|e!뷂1>DEWTӵɡk\X A+/ZCPrq[dO|IK QX>tX kwۗ0y#]j0{Ə:oۊaYԁ?NNtwRӼ0>XLOo~]"  -+]ᓞE{APeǤWW$X';ʄ痷uUB~CyRYL|lshLe㳇74#­:qdS>g_l_Į(F$ļ$|d1C<~AZ)ckҗ/57? +/b<āgvZxJ3,fKL 4Od9:п'͐i;Ltݪ T3T*?lbKN0|6sp]܄q슷^/me<\`AQ &' o@p,$$@~ԹY/M7{tThބ[y> U vqHRĬ wt6ǎ^ HkE?VҨ_6g4M= |:瀳G[!' ?͆'05.&YRuhn)N_^n) 1C8R Ds.2[/wI3B5=Uzd?914Hܸ:RnD[ # P'KkCfW/rk٘|64NFv  ^|?W DdT= T.A#0@tj hzyʒܡdz[Уy545{ߐ rr EM[{jg"DeG\KhD~;egՆL57k€|!w{#l[ސ\|_)Z#[is_)a^q7ȲHGF zيˑ ԅ:U9)UMmȻY ? |Oywn?յQcgZ;|sEIv2[LZR܉B " <5SWF,jty' 7mk" Cܯĩaőы:b]e#y} R+d!1f:@AB!˃R}-6pB CQP" XS-+˚k.U/r]%$PX}#@v8~~d?atXR-+_}BCAHL9=+&\G%FTj, heVbR;KDdyՋ#$96%e Y~H#-V~xYgn#d!u0DY:dh*3s`ށP6ejXۅ'39&l&JF֮eA\&LF|ޟ@Æ,-eiv"Z[Gf` q18]˴Ԍ^Е ,p[4=<uN7QıU}$˄-pGŃvON0ڦK:82Ė\Â`(*jtvHRF6&.c DBدbf*78gT_yNV~{e!抎Z2GyPiךwS%  s==(+yS5hƬh%jבAPZγW@D[Z/'EjtdɖP.²GLҼ"-SXwv~p84+j RB:%OfieSX*et*Bm_`aeW܋0a4/oصTE0͐ABlc/ amq+CIg+=7j{^If).gUI?f{7 6c8u7o|T_ȣ^I9wBV( O_Wt3rN߮g騞Sc+0|4.&C'jnf}v &Bo͟# q) IQK dG-ZSG, T'n ܬ7#tTRum}%@Ei^eٟHlWnDAf#U6m"~Uvᅫ:prFJ(elAWdQV'LZLX]3{OD$68*.JNэSgythe6G_oVHCnΌղX52H^ 'Y~"FXOE4&곀BovboykoBwɅ]>72(kC1FJ%;eAhwL׎zg#~ZyF1 +H^{%ȍqWl4.EGgі؉X<3)jOODmS}3鏍=(- O9ŧ&3G`'b!5`80َAGmN 9%K*5Kcg!@qs(0Le B=lZR}d\d0E~u"wک烱}F\@*v9 A qu1`^@7E/Y;pR{zJɤj$H zl&勧eJ|!S &Yn#hgڦ:j)ihoQ|Jt $^jTTI_F!]ZjXhB_N*,.yH !po2##8vJ52/1JW;X9SCZ;Cp6DtݬDߪ. 6߿`\l Y;dGBW_u$J9ԻQ)tz҅fgD[ B-]Wnd;/8D^L'u[s0jr@\Ϗu#L=y<ܖc_8"LOؕEeiʞwAE'fzf 31ѵEG?$隮VҮ,MI+.ͅ%A]v}A 2PQDAKxs}{9\sKlj2N :zYœtxu$-o4jQH8Af r܌x~,(e]][Ȇ\.a7qaâ"=3,R#i@i n /b2,RWvQ!/h vZiK#{\&Y$px$:>=F?/G)';G̢\j0GD@ 7e|?yzIXj.M%|2٧҈ c/-GWrqjUjw#S5^̤RyqF̋ W}NkP4WgOD[{'d]$y&|8-@%YgJV".+e 1_OSƗA젲z-7fMq<`cӅ7%`6^#Rמ¯!ҕR g{h\=<]bLڪ~(v m{.:8ʱK!p ؉MXkkb.%uG)âdOשW׭[|GgH͛{I0?>@+×MP+/ǾA ,0*a!lGom Z4?d}> x&$PnbTF,~_i-pqp"f% [ 8d=$ط Glt"wPƘ840Z] pT(_(xVl!ҧ,\[|g94۴h78n1=(hYYU<{!}HvVNoSGf7B! Ӱ2wHGvIvPu:3יB6~0l֟~(m,{GrXmHTD ʿ]8ewS=:͙|z/ }"&v>ibn8+Ei"N[M}P6OE!g/12=x# ;,]tfsQN ؀b΂'L^A?S`Qn.bB/Ozݔ"^&+s嫴;|`2zִV`3|{@(#<0c9yyQkj.r'xi~)ߌtBrh m.'W~tnx"j5ʝ7q$" /v߱wPeUAK d:c͒C2 E^f-q :J*d \PGxjGi S2Z9YRW`bxGkBnn+-43Ꮿ?mg#,[L2Fl?V}כ%u41WZfT"Z܉ 11 KuHWSN 9GC&x*+~#>NɎ$e 5v[Mcfy_] Cy(O L?"KjRO\uTp"iy`$e] Ϲ.K;{U1UQ5C/51%# EKJIER%Aik:gVFK{8 ad \|9GuБ `:)۩!B-ꇴ8l7 G6|f; xjmUyأ8T[fzX:9IؽB c82Mz+]ѕsQݹ``eCFp,c-6~ kM>!(e/7c,R} :zқCH?{{`{7 pJdR\=6?;|N#I Mhgҽ֞ $x8rmߤ-K.EjIۑW `-ObcQ`Y3G/y0Ghv]oxD2gc= Q.bpXc\z2ڰh 8J v\%F\y˨GZ'҆/aμ9 H{7STxI[Տ,^}Υ@ˡJ ], &"8*yq̊~z0n@X `gJTǰ? ATV,!#8j kOIB)ˏ/3ʭ??v&44~֌WQX6oz@qj Fߦpd%֔׼d`wЯJN l0-8 OG?у֔;Ai△|b_l_]^$/;XiAMߢaػb37;q bW?ėB|D2״9Һ /X*c'3=aB?Q.muoEhͿnK|&}ز {A@fT.%OGv3qi-3 ,P\/_5\(BSAea34k|BYZ$ڠv&j>9nfZsXQ(o:VO2?8lD6H̄emPj9OE_/? ɻ&1C"nPt~Q0oqlmŴeZ6c07cm S1_ qHsi KL,XvsJ#CF,xz0qO7Zz }Q(Ŗ @ik⾚H10a>&:| nA+is1(fu67@kӖGd߫`CuξDC^ow4X= i?\y?50)A7^K<$4K2Vy43ӍUVi[erXTr;vlnт:IWM%G%a u`o4^]ڤ̉ KFї&09'EOָ+}.5OGCZBjmG\Ӓe P{k|_Ƨ`,i qh_ \aYElDrm(jmPʮڴ#Kr!Cşfx6= +Io5U6sˀ׶8r t;[^ 2!b^x; >@RMn®<($Wl%n^N;Aѽ;:vDZ-est3r߷rCIq%r#rH- ʏC2yҚ>ž̪TT|؜>~l2Fc6!>M_ROP#Fܡn3<ޥקYUc1ZXA]5f®;ޣypݨAy1@/ķS -ȨipQLG{ ɛOyD9]lŋ%̈́ PNHcg;0!cЇFz -Mb@ebF"Sȳ1VFpӌa"Or;z0A#mWR%4 xYӿ,?w_DSIup<ws.&er4 jc4}2䙉I}>Mq6{h>>68GUޱW;8;~d|AOKzF^qoEdSGtItyu/AB*<2/Hf̚+n⎲dKܕiIDM1Vs xL`֏2lxԫh5{kdigSB~nˌݮxx2~g)s>$&j|v`9V'./ˤ.'N.rPV' @u]\1 ߈x6a^ÐI&'O[%Z*"mmTqiȸON-{Cwk:fEO.{:LSځ-HPyVsǷ:Ӂ"ԱYsU1S# {e9(~ɖ9h~CSy7M-]HA.~R9'TzⷺW=ߚJK}{(5ٯpKo# }G'5qg;Vk' SRYH{WL+Ʌ|6 \,cXgKw+ctnZ|ѵF=LE}ֵIϻ6+(a aG.+ mgtyLJ\:RJ(qoe]'p^b}fSc8rk_L#)yH&cד z@٤fwgxMtljNՐ:#3LK"k gN628}ùoEJ-B \B?>fy9k:lP)iq~*4\5 TYhf78L* xQ8Qu1֨_P18G1F7_K Ua[!])\ICY:{UNAyd:ruftV5(PS<{v֖ge襤΁ch14UB%>ZN»X1x=ӆXkKR>^ 5y)Đd{ϣTuxh@8VֳQ;}8w^xށfh胾KEh0%iSCX65vqUKo{|e p{,f~EX$~ 7'>v'z<1I c|zd{굆xGNa=JG / i]|;0"nh1ROfqSX4cHʕrZ ӆ vRg3 Q<û1gM>:}1 *7~Bl_}&u0D<9!uֽ>zP]p/[P[ަ"L0p{YUTۭE p>1I @hGIU茒ds@"sKmxP64J L ynѪd#ni%#-bSP2EI!}.&qM ATPhq,j6EҳPHoZB3OjQRm☿HBe֧WK\(+Og*C Dl x/)ط~R4h؆P% !o"xM,W(BS+\* i:f2Ȕ[ ^ (z▻g[X󎒯aqEڣNn {BmO1Wr'gԌ6-/ΞWaY)5[ܲg SoJnk4'}:eE vhITJ sㆃiO> %)y3L=QѵʇRClJt< LLZ'<)@R"m ,JZv*}mQP,r!J U^7qKU C/(-iۨ?5}O*{+2f]K(qb 3EG=uY!mexDnHd*8E'٢ dio9TR;=+9)ilqB+afnp%EIQʩ(=$~ҳ`wQdAF$Jhk&^L̀rv+S^7%!tSm Oۊ|$xǛ<+~Yfa34(zkkmn[l=BC^XL42bsz_M[SzX"HY[aiޓڬ,4ڶHtw}ƳWC^~/ tpu_0'"m_[IE@V Zɮ:_(2CO2 я4 q O-2zDnhϳg EO oC'9IO:BC,ۿdBiOuOUS] ǟ +֦gd>%st_Q۫{٨lm yyN?Ō'3L~F nmmv?y2k PH}@/p+@yy.3{iY}4ր䴯j*m1=4nW3ys֨;/K\rxy!u^b6A$Gmne4[W>~S]yPTwټIT2ejk6&tn^WjgcD8fvwfZѮp8f)܇Ҁr }\P@u rr*ryGշw|=Hg>NPy!|=-$щ:iz)uQ_\6HBHS901fW/eG"Tt|002awLq B[@|G*9I9t]`D,0KW^^^^HfJ,6{_o>YS XdFQhkqjbz|<.mc\)m"?9X0uTBIHPD/ZwR!0POMf͏ 3NQ uRȚ&~ T|L^mowV^~2VEaB%l!JP6-~q%XִPE%yqqNŸ8St)lRgn 6ijfD-c/ߒA=EMo|S$F$~d`kLMѮw[~ }[ۨkI~0;١ }FTBՅ΁o!.'75۩W-pizFl%EP7F;PDMaQW| =>]7j0vw>5W!0o _ZyRFǴT@`KKCr@8l3Ow'מu)zxws 褴1s"J|ajuP> 3q:bIh"}{ȪО6bȔ>n;m'n`EoN@NO"Ҹ7^|eDq3JvIL廿#iTBh[f|wT\jq72ٳ4F̸N燴G#Hβ\D(O@d>mŅ/=Ps-(Z&qp-kPvS_&;x-/G2 g ЁC$b;~y7t\Bazc=H&w/F;3, =Nv!EGdp(52^CsEW[ %GaJ IjiX[( wK.@ym 3DL\-ꠋU$yUpVyy`$f3.RƆ~IK*a.WՂa-* QQ5\D*|&NNM1=픿6t%7v:>GlLc\)W/~2QeOCÁH"UM/ A⿛Uriހb~oܶrLWR^/My줎rV zΪ, mPq2^=i??i 2egtfbg?/<ڕحT3 |.l6bygv7_h;7jp&Ro_;v|oLF/XϦ[ CLik9g)w`kIG+%F/҅R jI=?v'֭e騖p`]@d>Ymm}0_OdHF=U]6;YY_.*)n"k a7ٱOHYp}(Uё|E +/z$wM*d.]{*s fY`Y'*#;_޻飧Vhte{ a|Z$Z˟9ڂo7Dעj:?9+]Μ3şt,<u=.' X08@he2j1)+:~YBQ* c: O#s's{~\ /H.ۋ#p`'=Tʭ0wH\4͍ꨡmڀ\% C`_L֬P{?x").gܹ*+Zgr@XJEeluty~sU WfeW#H,k |S, ֎97 LFSO ӛh^f>mz;F{* kx2)RoֆڇOD=ruU,%bȄ#>xk ?ށ5Kބ[{IaM_CvK{YӤ+A 1U?UXw` kw\ mp^<¬_Γt.! = 1Hn *ԡ_F~'32\SL3${;ߒ3>Bbgopʪ#xg?L-UMu c Ÿp,{ kLެI|,IGujtH>y Vc$4?}^V.1-'CN,7٬hr?L~syY0(gVt)հ, דB} o0ww()o`~`ٳ_|NfTȴ:SNB뉍_|'VBʳJ֠l1 !>!E^~a ˗LB!y$M~ ǓO9Xz,K'%'c)f*7>^d-#fs*4S؛]3ȯ-N|7^CdF?QeV^ȲRw\@; JRSh/\{$Rz /Zb{+4)ڇULnv2X=4 &~r32jn6m=><Ֆ1v|Nx]ڏfSgO?37n]ڑQLuGG4chIC,pENjkB8Ehh6ؼwIT|R &eb_)D;LL~q)^5‟m$=Tiԇ-'WP2|{Yf\sb€taɚmÉsFo5ꂎJ,ddȼYEU0'G,7}fG &poAOxyLiRO&5,CsaXsppnF` >\§踪tR@CЮss$_o LڲFh;sToze vPMlTSF093zV* Wz2  o3 o@Sڥk±AoB`Ytz10>$ʹ+7c?tӆB/[`3(W24MOMFǴ< #=n؜As+[iX,8j`,$ ۍsjO|'x1Sz-4Sd0ubcJpwS9 72޶oxXuf1;ȨEo-h$Bp NC__W-ʋ'cԦO.³DR8{B#026W98tOޯJQZֳ F 2`YϞZ$dwi@_Q0oixx%*BHnhkl\ egdƳrz[K_"G'Tsk$==lqct~u:ZCH岽N*_Xx%|LNVV[3QFqS2 8syT9{/KzyKO 5YhhpT \]R M brM"2 a>JEo6 g&~oQ.pW3RQܽF.b\U9SKGX".9 {E:s[[@{`+) XHٮN(E =g$r^~6[S@^X&R筄 ` ?"3Znҕ%Au!mrM)[IM*GM͙tmۈ&x wΖlՒi|RnjZӸ^_E?aWՕ0FLN3sΜL;EQg2%&Ѵ:;3R*JUBbLR &X.(KUQ&Pl{Ի+x/_w߽ Llm,ONO`$ʐĖqX 8!6^X A_N*'9p\ TF>, Hr>.b9BҫbzͅDJ%{|1^C)!cpGsGj^,Lb%,s0o{Nh ~D6OH0m x|K8$Ѷ?igy^O1P,+K P=3R_I:lUht$)%O Xnh5H`ο,+ɱF`W|_FQZjTHrK>2!)+H>Oچ Yw+}\ /Ų;Ux J]jA+^;ȍC:Uxr>-hMOu>,2x 0VG7嘮ӱ [Cnkox]`s[vZe|ⱻ08> ͥ JVF2e$faIW͑j1~fio+mEw)G}o'<}iSAP*lu>2CkHqR/$g(PL\RbH[eXsK(SWrh[/_;gJ/GdZ(?ٺKP˝$a*ءMyGXRP2gdJ}Y`KY{q݅~h]wxdOc[0#OD~Kqs7rͱ c R%<H{$4&BhHUxn^_|2ɰVF|xPr51 &&<(;<ζzG2O%-W/T(휼L71H*^c*!g_Kͻj''YkD4d[ݡ)yRWt][pN5!p{A.;'C'ڳPs#X;P=~>@n٪&v{|@<Nt/-Cthjn ~}T:,dJ]t{2GAnPUUmT&ق+s&[ܭ$yҵlOh(HD*GR*'a!H4`E uWl ,ѶOjo܉PS|1va(OO_ BR(8{relO{f>HMVxdUBNUU02'ڦ'O rыVԄbPjw Wg)qrkAU*hIm0a{X?},y<;JWܰτtMnͯXE;*xx}!$̓suC:)詀W}'{Cx_yY! xj|"yMoAO&1Y C_5,M" /3'#K2Gn[|}N"cfJ/A~M>vlhфNJ+/%6jKcǿ C%=RΥB@ d.8gzuE]YXyDceK ё"5df8;z [W_`<ƚ.}%7#XmaS^bmS^|*HlG# S3aGR&Cdn"(=FX< 3n>h+gK-TuY΃Lyc p2t1bpDM,%l>tRΟ7KdXYK$`Oib:]{e(Ul.IM1AZOb냳F8+d tys/K;I9"Ik8[D.Wl@*޺44a~V4*ߒro- 0PwbSl &-IlSn]FOWݯ2Y Ap̠'oU qVH-% :+[{ Ԏ.CZHn>N8Ca:Hrϙ}z43x' p33쯗ȨV7gPWo_+sZMWUd(oelFtW&& @wJc^8xxāso?Ka&KpM,D|=|sf C6OG>IMbapWޑ *[/ C`"dSG(ph9e2 q8p.vn٭8‡/xnoqb_MБ׺:gm# (dxi1߬ ?d:+/>J5Wm&oc/pj?d,讹OkӚ.଑L,ܼSlLxy{}y7/`" {%Vaem-<+h-F_fF_iKJd^J͛U9K{D6dN#lhBh+*%Pu!B@Q%7gFJrg^IaՆM?@bZ\0fI%ϊ9~O~Uƌ`Ed@YO\2;ήƃ+%sA#eAzz9K@&y} XYtx7^:%] ]1ؘ/%.@Aٟ pbHVVeh~ #ᨭGB8Ch#Xq, ܘs8}pCipބaH˭Z *Œme@z*+@Ig*kw{1C$lC-v១|L#߷>_:G>`37VM&Kj_;4;k|xޑE Ыk&emK?=uCHh5R~C9~6/U:cSq$?4R]1cKf8\2!׉Xgî"Id]J{2Ik(#+$oHVWД2Bw7/S}nq;҈Y%Ts`،k֑S %N)r/ѳM XWڲ-S_sc<x7@EɯrG!xo CS_ 9B/_@Z> ~@v@>Z3|u#Dv~Fi6u;'Y11~q256;Ӝ҉"GW|2"rG9p9,*!ukv]fp r# gr@eGR,SU P*Jފܿ2{1<lXC3g}f@ q^[~j9/yO!Kg_K?nx _ m7 3_99Jć@I*` z=abi~KbRlݫ__~dQLKSvgoA|{(60h*(+o]&vrO5O<ȡHA9ϭ+ұa+fL}#lT+d ,IվȑN#8 JE-{ϱyk"EdV3MWa O]x1;~MFC /4] #k%E6;3y?3;;ΔHBufvgbǎ'qo7>O#IUt{#z5T1)6# {$Ώí=K /aۨ=/5;A dC| -6y\%g^ O%PMNA-\\q iݩ'="6W问z|kQg#8z$ =H,VOLݭ֑o'0&LV[W{ҒѨ!1dM=pU~4-Û  I_ɨ#_A$PS|dS*L B+ ej?$O![:)<ېмlEI$t h9͚4b Rc˫SrcAhPϿݞ?Q۲ofӑZQJ'{@s2c$)౧Bւ*M[VjLOZѓӵTjbuJ?·Y߆LF,LGik43DW^ԪyUM9v|{Ί/P!1pOGt!2нm%E_Y,! Eg\]eJ M], ZN򭺖U_㖋-'RҖ[fXЄWFd'#u؍QobKKHk?[!9^FA-g#i7Q|x-UH!Dܸ^B:ɳ6Ć݇ǥ\=|My\F%u$sCGJщi5VV_|N[>H]#}p"-̙^iZ^Wlbq!sS($Rc9^D_ՈTz3d]os2>2ݮCfKog{[`Bj w_Ybi5ZDN9e.91W)%.+c\:-g%7jRӮϻ:&P1FqADsLnTy89hX7[u$08GMӧ8яÁ#sc.X+SX,c:{^UZNI֘}dk2utr!!+?0igl"M۲4-l=]te xRډ֔L % 8`6ԅ ?3ӳߣ)HUޝ: ; nj3X;tT:]t`&YqQʝѯu` W?9T!/dT|cIΊ. rG|ȦcN '% ȻnbM?seƐ탶FC4:qN9?ܔuI E>q1oɟwHF+1֭vTL,eftoL`{ϘLgh b/1mS*L8ˆm0E)~'2ؕ@|ۓ±Zr~F{7 N7-+Im!2kPqLVW+Eh1ɣ@'ey-q (?'S\"۫&PMZ"bӮylWcgioq_hƬ?Pm>:@ $dj7ؾYrǎ0 h f&8_bw{J4;EϗyWe5ke:jtps\#qN)]*ܕeOLfӚؓ!VbRePb}Y&@5#bXmu>(8?-(˜X;#hdg Ƿ>x NiZw}_YӇ2'h{βr_@I{L&MGNpwde>y3 |an|mGwȩXFksDdG@Cgjl;cl.,wόaWP5ϭt2Yt^oe#_Я1Ͷ7*fK׎˜u9tS"1V;}R|S#|VB1:ys"t#?e'DfM Nc[`UP1\Xrc98"3ooҚ o4B]Hz}/!2ulk NC*XC%o[C ~~jeuNlRg8&e9|b  hX dpVV_Ie%cxMS a EX[]"z9S;cl?/(TМ Ed(KϓD`үBp7H,ZӪ`vB?7~M4 _14j6sm ? ҙQ@L?׉Oft@ ["U3O3V/Uoɖb$a9|. ["S/a| xecPtǼ;[3U=8ݻr{۟08V3!?[ijM nW3u85/;½vZY9VswJs= ;1Gx"Wr26  e ̥$RmVί=p۱Bsمgƈ~NE%%Q&wDh'gZJ=M?AA+l\Qӱ7"[8Nr=+g4B6Tkj{7uŎdD|#Jϖ|OBST;텒5٣Wt+4'3a.- ~vz6XǙR=;#PV(sڸ94.X֮e7=ՂHC?XB/PWzKߴꃪֻ-@v U70 f?*Y7"8u$|paA~NA :ݚѹFyMggMoyG J a}nPXKEذQAEtߵ:L7fGÑoy}U -Dh}D悧}E>U 2@4'uAB̷6o>eX|Fv2Di@\&#Ā#yUq6J;:pq',87uK!<]|8}Qd;*{ O408Z2%hjقũ/[Y餴| l\Sn:12L/Y@m/Tp=C-Isrϒp*K:DHoUs6kI?6O(?|SJB31;>›E:NcIlI0x|Ј5 1(֫W[r2cGLμlЂ;Wez$3M[Kyh4-pXk*Nyє>mD=-k^oTeYpVn9 $?ǣDGF߻mwӤO Ԓ})&O+/Q;ѩ͂ޑUN^>@\SFT=^7JZ WĪߙi:ʇ7݌|y߽bø|d\,ܹ8=Ǩ5B"To/\;+ @>bsʭEz0Ro 6ʜ\y̕~_YgHE N^w]?wd=忦YEFU!t2,Sg Gz?7zx4rf|Ɂ#`7@a:YrCWBjXaL(Du; ט/Y-P|t\0j[|'QK`Ea=Ƃd/,wSܪY+DS;O+N"vvBaL5M״D@8[m%! Qk=89C mJ2$"YL9{}#;d j,mmT;F8]7١;/@:~ x[[:.PFvyg$5RfsRbi%3F79)u*`BWrd,Dh fu3eqp0ztS:$Blsb"Dge_ qڞO[{j=l(qb : wfཋ4;B.hʧ^H8G bDG复+M Oixz5D\)/T違@ZuTP>Uc{!A`t_h?n`>Mj(-]+;f3YInOy#UH[ai/GtPWSF{a;ft?}?%53'--ueE\)9aIUJPK,`sBB4q[zj-M]4Ӽ9>UDʷQ3C>>rmuP!WK-VVچy*`oS H By1s~(q9S i(p}9 E#A8S;S%0(W=j®3| z[5eVd@C'N$4ráߺ3,%nj?6U5ݬ@49ہ5zBz' $'N%IEu! \*좰gvRڂa'<;QسfB31#r 5xNTEv/b }=heea,^e؄^؁Ӳɜd$g{hUL6.q/4ٳ GSk~BxmM/TQ?@-PO`d$ dΗ X߆-dύ' kEMdsAD [kH}jO g, ) p:[-5(qCgY E P$$AU]W4TK C W>]v'JXd. ûަ|T5)1`Bµ9Rm\1U }aFo},5T][]4B5;x$/pBZ"9HN DىO+JV$ +${o3VR⢡@䛸]G66~$Ws0.s4$M״C=_fU=+I\[R~^7EphgNOI~}^T*|2'V$ _e=jH +[F^#fzt9J۴f* iX|+OD(t j] H${ pٕcA2'郛ZCS=`Ho?e#Jv}Ma-Htjli!k&jŌyx 37zlsR߄u|R CZVX[Y~K+# ])e=QHvwIm)XIl+)kS:5NC3 ?ta]8$ewHc:wJp B>̋?|_YؚEWq*@򜌴؊q. $N.lzHF=C|LüOG>7Qӥ#棓_|pb߯0mVY$ E'aWSXY^zFJcov5`j>> t?'8l{,#agrA,JJ҄-=xB{ 0:cR餲֕0;55+ mM)#la?ISsPƭE!X.|k@es%騣ާrSA\H]%ȶ|1'"x{tjQ сJ}L{AVw*E7!P-CN:>WņdZr{JWK=jg\AWV6ƞiH N._)ٰ~N3MMYR=LD5aH! BH0(mxMe{kH_*)pd?ӮmjD%TN\*="S8} 64\ȡuV޹1F'O!#Px!+ L]p[yp%թ}K7`<-Y?l~x6QsVIɭ2O4@31Ö4 Ҭeqza!xu5nQh>bg:$w^Z{w$aڿu2OTIJ\aŏ={1F??On#pB?}E:p y|䬋KȦe-@F9#/lA7Ru[.pډ1M//|qtH0}m' h?*c}B,@m!Hjouǯ#2h[rn12dR?{ >,9tYuOL: &k>-~ﴏ\'՛ ](R! ?H%k2G cI)b>9ji @,;c]qg-#s 38MwJغ?ۑ4o>\U?cojJ.V ǣܾ+])=\\Lh j";Fk.6MgLBSɓ K03PП?lQZCܮʉC7Jp.:pT~m)Wb1YkKqh<0Aq݊ю.="hqwd5,vA?f˝ a@!g/ ueurOnGxe==Z\/)@_dƘJFoǢ{]$\0S{ƥ5?153C I(w:p 1ٞ9;ϩ# 矅c x4[~\f, mo/;tI0R@#_9}+4O{cX8al3Q iF;>)_eG]Y:BYj%BZ(aC&E }`Vh/\ f$~LѪnlb?(a)߈ vDT&D^Dщvۀ1ʐ5_CPf[y~jd $vc܃/,L^3~,¢qyB5h߀TO)xT̳ g!ҪafUb taD:YQn#t|GF57]P.3 %{.S_eNjD+AHIY::i/OɩЙTdJo &2PD&wxoNub&w^@+a'⽾-rT:e-kvߤG~&I* mI򨌰;(jgH:<TQdxx:D7%NRʄE#x?|O] ѥ4y1ؒq=~yH2VxMg6ul#-Ɏb% N)L@&p>9>DU{a@Ys Z+Rߜ\E3$vq(V ެwhzHxԽ?:-5=ٴ@T6U7a,]\H@&ȣ.) DOo7IUxURiK8k[J޺KabJCT|eX.tOiؗTD p |CY #QדYjG\anbRq7:ALٗ-p4c$? (6NmN z`~FY9g*jN +mNG28oBުZ1D'7b.I77wOsghqڕ]ʶD#Ho>'eQ#M3UjmuJy\hs}GdO{@qGC6Fa1;4=7HWQg%$VbBVp]N.2H;;=6 es)57CQٔЄ~i"[zyb›=JIT_u,<3@4P# siv\ N/ 8ێ/87@@%Z{أ@W+: (^nowҖJ@-sv\ufuI_;[;|]2-urh_H/&ӧ dm m}zmGxuVm> z{Pi _w\@Ք-%/>G+c˓ 5=#:4rTc y͏ yf=}umya=&W5o4;a%ώz;|\NM_yEab 3OK-ƏGOL$pik5 *'" \jD(>_wt CgDXekڌ mTۤh~p~H6郗yUhW, ӗ}d\:]r#! 2tXj3ٯуp4x016E㩝gt7h,I]ƺnD/=k'nk{lKN,hRF눰5EtcqJ6.WrWғg$/`J ?X]oL~s)b7. 8%;ZhbptD#dmtkի0a1斻LXO>TZXjjuMfQD :Tg.[Q0hQL& pJPjyYBkCj- ~Zdru\Z>lOȎ?Sc/ӡJ@Dt䌇Յ6d O|[ڣh艝բYY!U?-{@R #C9BhA}E1 H+Ò3&E~CC"#:+us: V Ȱv-g!:ُ"{=/AlU&˯*F țл._| e !fak*Т:]r`[z9;/.deO* Q&Of(BP>TVvK[pzb bGb2U=G3-00.c,S!q-H1k2FMֹ$0IbG (ܯnYO=Bu˰a}%k4(zd혃B(JG'huv0|NiZX;hţ@ǏbjưȚ*ͽ> HNZ÷rKj܁ Tӭ Փ"nA .iHJSxf!yz 6̶m{eeccA?g5FTܱFH㒑65O\7ܱր3uќz_/)oE񹂟Ro_ąX='#ss(+4?^Eo[cEa/ğhEæGyBlEuE3$JYO/18&s;5#yRV[Fr&-l.DCnIЧ~]whZx#@]9Bێh_f<5-V̺ (W@c ^`g֎`|庄1ǯ22fzـIuN.wUھ[zƒI8h!N 0 +`,R,6_'lDd.ɿ8wìAw##Vb^jI!'F%罘c#ij2i5hDaY7wDY1mYuFD 0W6=H2΄"gKƙ=D/8+dn~-5 O"/z'Ge{^hZyk}5w뢲1R;dn S ItՃφ)h\Rkh..^4FTasEp2׊F,ecR>Y,C׽pc`&f`p\ w0@xv'EOoQe p`ЙͰQ'2nn|*,Ȓ/Pe(V>VړQoPE /FH5mØ8ܻDcъrp+39lR֑A y!04I9$mKSU5x:.sb=rR%NHDbmVk_X1Ül~#<x-/#rnSu1Zug>zbXmoh_?9D,K^&{WK5=7,@@3˽J:b+Z|mAL,p2 "p3)ODwH'¤~Ed~5I `" x֨t*;I|׻I9 r*=&޳j59 }xlXY;U"ADvz_%c1s"wY+ SHtmyӮ.eб8ΚBI]i]E_.&U\dnmѱ9>nH=9q@vDL m>Iw"~u)2V2'IudqG{=Xvn"l}B2,ZI򮜪*o,]Qs"o@#q1nmNH)٭զ*0A3Ux){ݖS WOu9K:UL{kvģs*'nhѥKz>1P!zVc-!F5jTOd(|}:j8 /Us jPRo/7izҞDMiAg`*7C;GTLrBe(;r/Ogj酜 k.!~ON9![/I )xh_(!E\߾D-a+<2Dc{8[<<",v,>Mt")f8h-{NYI Ť?'v:yЏO. "}Kv o k X㐭e$VT*щˢ۞ܟzlZj1Ժ%ɚKÃF'KOcv .(7X!}%_ISA4:?CÍR˩ABF бVۻQ#'W2O jhOE6X mb2$Ff<1LNd^^T߅N;[;9Eހxqk;r2qnlxrM}Fn8㐟 m~T<)4 6T2E9'Z{aDRRZ}BTY-7h1lIܼHVX(:AphwW2l1\WY@esm.  rHYKrqRYS dzs8 2/Sߑ׹3RJ nسδ-Gj2 % d[]F2YW:ٖqu.߽˜)|Kuhl; ձ=GF*ehoȍ,6CW;bu1q%(H#ligx:yϤ0PUюt{# I)Lzʹ~TI>e?5F9zw.kHWfW!~B?%.4#+5+aOQvIa1yI=?$Y47P8wuۀ0d,q" nV9'@-',um*ۨuk Jx; r=L<#ZnO\^}CKJ|]%O1+~SNΤ)+|)^H jLnBfJY:5Tlt٬>%BJ] f1 s6?Ġr*7. x\hDÇю7rj+/tE{y\ܸ*!L'䵑ԧ{" R)qd˕vW`&ez`Pq>RaH;dP Rx3PI(Yu"?M^R{zѯo <;8lOaV!$-R J=|>ۘ ߊh?{2PmYS}\K?{1 ngdralF&Ja5yX2d~@#jwɌ| D< !'ڞg5Um-L*ÛqCHx+&f74AƮ"fy<@,WUDdo0Q->Τ;hs"S.&doPOlɝdqh%)KFi&Ldbġ7hzt= Ntm.']E&(vlE yIuHlL&b MY=ɟ*䣨GQcrj27QH{o0[v7;ArTܔ[Hq-`(!4!0\F>~z6Nz@7:?āBOhPW/O SГ(;ȷش&ZeHOP5ȩ0 gpk(@] rj,5hp6&)3 icCN/MY]ejX~>L3\6/QLf9Mf-$sA+DF3c`nSt!6qauw\E dIN`av Tp}0]]k<ߣu, ̟ ϏcY[х yhǹj\*-?nDNUDT'ipiQ?W23wSuQ ɜ\o M,l&ph2/<2nOU,ls} 񈃿wN⬎p$\v+@!q+WMZc{6}| ӕKj"3|/`s U IA0i8VvSrD*F~>3<XThw4,BJorbFdب19#7ٕOỊ$Q]jj s fB"|0ֽh-\øuߑ鉴XtapEOb ?2TgW%,|2OezvCę4j|hWo\r;94t-#{Z8I9qY+b߱0@6&krlh;h %bL$Y/|}[ n_+k0m[bjnoe˩RzT9ziB/1kFѷdrZtӥ5jsAt$m XixkP0R:w [-[e§d@G݋9l ft+8I WEhoxQI|:Y-Z_o..š? k6-4#Uh{2wwI({x `1qy \cɭ9VeouhPx h/b&L0 ͼFg)3TnZp _=ѿBSA?zʠg ҙ^^) <ίlӠ3d PKqE PQuerying.gdb/a0000000b.gdbtablxUT \T\Tux sUW(`,H $-NNBRRKpC ܹ=ӧg}:3$7qODT(xihQoPH>,AЉ<$n+W1%c5 5hP*1 :D2A8fe" 5b/bJYSNQ`W0hC/*JWr[9p+F4%Bb6 HGO}S3+>WOQ "Dx bDXo.V·;{V+S@5OV[Pg bxSq~3T\CAeCq 0k8Co&&1:<v˟>2$%p }F*ۦT$-0E"f@:3X:rr-Ulې<)6d{ v,j^p]~Qnq-fzę" 4+ ֡D9Pe. zd\CShV /D-R1e:A% KP{ v(܂b*pEZT-M;*UZr" x/ :YK։i()1:;` 6lDކHؤ0bn,[ tjwA6osR E5 i<ȿS{ 'v2fq\Dbgq8Q4ګXcj+3V#`\Fކ=鐝{ꨦp)n ,ANZHCf3uS,CSM3蘫|!5ODD3F5hxja-*! ua]/ EAͨyIPc\6,d!?^$]aUD仦gA&*Op4/3r?mW؉: ~G: A;Q+* VbaMatD 0Ab_q F:|]W۰%J(pY&pu,h(x3kXbQ\Q>0WpWDxO1y+l䯤eEʁ[0Ū K%HxsU- pLxUiqcZԲ؂ڵoa- <kHC$3ɸ B@B'0yX lи^Job1yo h4z HC7 e[Lmp [IIG& Q,D!LUtm<51J;G6jň10'ТSkDbgɄ(Eb:Ko؃7vF7Eh=0аc^1z{9f`@_,B-1%J{LC~hF[S{|Cjx pfo͑wn$e=Ի"}18>4{B@99]F!4y8-#j," <5XSDr IჼOFLDU8A3s[E=(JLB7BKSH} +F(iHx- !8noɛ/exmq]eD|At@eP##Qc-H$0S;? Ũy# "T1U2TlB N7. ~Q;aMhUE - ` ~T?:$/8^E_?:Ƣ h\ lÆ[74p7&:nfq}kǐHet*Xװ+؊Q 3Z $clcf!ϸ@? xx&8d?瑎w'TlZ_,Birr2];#:!z` ϲ`͖G;c(Π\\tj+v'%pB-n[ :.'{qtX c7,(B4\GJW0exFC@5"sQ#ftZE>ӎ}q ``hlnalDV9`Zf9@b#jm?AOv:@.G De_Dݭ{ D~U Ip:ܡ!5pwVp]@ 9N}jhp\b7jP( 1'1EOKEܟk(qZ0x gSCsP[A uHh7V$^S,Ae#LC?&:^ ,EUQ6Ծn&$%sHD (\D(S VDD>kT0:QP d!!2#1*16JӘE/c^2Yǭ%QF1QO`5J.KtB2nqyt sh{).01\`6B+lhT1K|~ʁS%p 6!nUSZ`>k+[#Vn؃WkYEځ?p- =:$"=!2Ya"08RC64.FƆj&#`%xsQF:RmxдEW@JK\ru` x$E_6AtjU,GL،8n ]QNQFdPfqHѿB3#OO+{iu{~~#^p -n E(z LUswmP{X;8 'att309hub/)؅{n7}qta/E(֡Ń!Cϱ eyD ez؆ڏ ~D'`*?xHx*Piˈ T{F-<{P9z|Sd#8/GKfetļ"1 ~ t-;ruU،oED#< h#M> Gg*f#s3ͨ"((TZb"܁q]Ao" Iy֢v+dwl@#?a(rgjjLMA~5l䘆ryG!>t*^ {h"X#c*A8#9 o5 i(>J-w`D˱lCqtĎ<(1!0q =88Ve-.$Oq'{!qSvT񇕈&k13d-͔8ֳ [QsaͱX2sE!b_E} LIILGBQtPe5G&j,/X2t\nAW:c#꤫@U [Z`)fPT_lb(NcS.ͺ؋5 A4an4HCM1gEnjꐁTB,Ela l|r٨-p-v tC= ~{FF>M! +a#Z8nhNtGm~Ĕ  G,Ac~>_4 |L$wH@*pp"<)9 ɧZp-sΡiU;PVT=Ds+s{2PEF..eq 48WP調G&'by`2 ܆(?04(؀ja9* <(z )t,дh~&:oc<(S"XJ%`rLG|XњR)cИȲ1X hae\.0Qu+?9YhЩu3r` *:8T ЪFqM "wbΣ}KPq ¿h8>U؅2PUL7 <(\ aJ܏I(2 _+ IiEX6nRtQHRĤ^fjgk GѴƿ1Y8$Nu+Sh* ,[@wKPGABខbzVQŨzKUA>FT$b5bo LCR)(c6XKA;Pqw`N#al$xH/&"~a;} H"!WaA4|DAT|4,c&:>P[XOn4}J`+̛Xg1yU 195|,G (XCK8/ AW,$*°^ t& *2Q;(HFF:?/֡OgF~XRډ1W7 HGF,RP@⇘qLCP+(9L{3h=\q4؇#3V#/a.HCt;=v(6:۰)cT.Fױ_gb JW1 '8Ka=#u&* ؅bO6aX)'0 JEt&5]Jb0h4z %t"1W؍s Zϓ)A"cQ,ͩ Py|TZdXB#HZ#p)6rLTXᄌ _\c)J{22FՁ(ᘀhvZ*T61 Cp]k{AUr$mcoRð6?A-؏[?v6s؇FmȷC% iOFrhQ@9nvlD}8vv!>;rMH>48CX1 U>ZTPhsBp O:|"pL:<`"&gXZT< QM[Q`/:^dr$]R8F9 ^+V$\XXh!#o@ B |N` tDDoC(O?! |SW80V;R3X0 J!,cJ^C6D~G2XdaOc`?G6^Fܸn\7׍uqݸn\7׍uqݸn\?PKqEAjQuerying.gdb/a0000000b.spxUT \T\Tux T5A0^T@TTJ`.PTL,@DEP0KC3>ٙޙsdddd`EVbeVaUQ=jHҷ-}Kߪr}a:e6?sߐ^>]x̑3e|;y,.-}Kҷ-}KҷmiݢeQߗ؟8nt:ġ`mVfds7buO۾o2l=>wo R5XFҋ~ l.2n.!F3'xxݾBx-̳ rz͖lNBt)qqg鷧)@|.MoqvwcBXdq~O/\j:Y-ؚ8Ӱ R996ӬM֥ hD&9S@!ŔPZc|?c"}fsIGe />_O|ny̢,N$%K,Kqr' 3/U$ R0ɢ$+ȒH.fsKOʗKbeRʢ,N$Iѹ/FTyIW1KSK)sRV1Ke wu:3z2Ǭـ&lFl&lJs6[5hC{۱#ؙ]ٍك=ًþ@7;pӃñdN/я L3 ,ssrs rYIs7r3p+swq#xx 9^d Sl=>G~muVm9oe%ϵ͏:lִfwqӓcũ 1\]iJkҎt r}8A(y^`3'(a=!uO5 ˺v |3|_eyNoe^g:g4K=9 XiAKڲzmhxoy0<+kG|GGv-zͿ ,Kw..fn&$O1[{~c%u"N1~Jg{9:U'-7|m㺧"Ĺ/ÌO+qD^d<:񪼦??YuilAKmykd w0.ګ(k|Ŵdz-q<=O[!NNo-߱b_ [ZXv\m<*^ ep3|ŷ|O-[::|ıOX=}'}}6L&[ܥeC9<&[+ўmNuz9󹘏439Fyݢ$+ɒ*fcU!U)UIfWI%)YXAd"fI:,Ml~Yw( 2{13̯$ *J(, K,K2'/ G(%31)I?%̂ p1($K/2dfJf%SMɼJ`)eaY8ɒl())S2'ܔKJ,dF6ώY8pEVbeQFMjQ:úԥ hm&| (X hBӨ79-2-hvH'vbW  3k#u:79 iK{:Љ9ڎ7s=894q/1i5dmR@ MFl;NԃMfo~Zh%jPY7Ѷjԉ4qbRzWx|74 ىc.벂&w}@/6F]W,8)U{cnmؔfnte8Nҏ L09s\E\\Ռ۸yy'Qg^V?FO_vZxsSv\\m7l{bMϯܢ=_WPʶaц6 HoNTrsy,H~IY1O<6a7 O6}3Dzݢ~U 1 !Zy1<E&o1﩮tP{5s'RLG;fN2ncW~{/q=7p?xW V4Zf ։yE61Ĺo@Es׍mIGL5^ .rFps7s rswrr#Qk~G0)8W!s2qC8\B[j:r,}d*o2Ml?Fpsws#}<Ye&17x5ڳXhD.E><ך?JΚi{=C>>&\}\֜/w0sR27ɼEYT1 Sh)gqǵo&DZUJ2_IT1sKI,a.,dq$$͒dt)$0%dfJfU8s*J2̯b,gY(&T"fF2݌e?zrЋ9SiN?3 L3}q9G]ш|J؆=>mz;\Ìߞ8!ڼ;xM ]1ƘdAc#| mTOZ:80.G[zڱ=2բ1s)y1Qf8\u~#s?8r;wr711e$OE y&1Wx`&xwyÇ|g|OfejTg5jR5XYuK=Ӏ4",!| (bJ(Ƭl@!1)hf`s%[S5Њִ-l=_U3e1Ft|9-G ~'~.RJʹx<8?ƀ$No?ߊVlK'gKJц~3fl}/ѓhC33{GHG>+'Ygcě| ޳"+z|XzdKPJhK4ac"~O=86,agp"'ћՔsCͱ8ўIY´z?>cv6]Zq9<92Q񛏜Cg|礪%wdv?׸t>F3)l>ɖ)rNw618'>7tsY1~?9 ,.rd~fzr4}5~gw};}gwJ-|;Xbڷr'w1Gx7{:vAhƘ\1gI{xdSc$~4`+vy}9{̫ S`eaYg?-9K)d~,Lh137d2μ,әtcY,b/+O)R2,\FYT,NɒrH~Ѧكٟg8\Mʝd cyyyx?O_qN jԧ4CԷNq=.~(Џ `g2󹀋kW;\QGEm;17b6SԊ1k&R7~ˎ4!Qg!bʢMِ.BќhAKdkiO#̞>H7`pzr$Gs,ы)Jҏ`09s\E\e\\5\K97p#p;wq7p/3y8Q,/2Qmb|ؗ1Q̩tsp!p 29[gsr1GxohlP".ˀ)qyn9YqzL7b|D:G|mЁtHFnV^>[szuf1u)1?Ёbh 6ȥ&? }y-?FV1*ɒt3\Y,,N$%Ĭ*d<1+ؾcf$I@24s>Mٌ]8G0,JnVÌeQ׈1v["<^ڬCV+}(ߌ-}Kܖ(/8%Kʢ$R,Iw:NHҷ-}Kҷq* ѩgPK qE./Querying.gdb/gdbUT \T\Tux PKqEs Querying.gdb/timestampsUT \T\Tux @ (+PK qE AQuerying.gdb/UT\Tux PKqEWᝦ*XGQuerying.gdb/a00000001.freelistUT\Tux PKqE˖Pn!Querying.gdb/a00000001.gdbindexesUT\Tux PKqE.auQuerying.gdb/a00000001.gdbtableUT\Tux PKqEUH Querying.gdb/a00000001.gdbtablxUT\Tux PKqE(2'<Querying.gdb/a00000001.TablesByName.atxUT\Tux PKqE0UQuerying.gdb/a00000002.gdbtableUT\Tux PKqEzA}n Querying.gdb/a00000002.gdbtablxUT\Tux PKqE)!*!Querying.gdb/a00000003.gdbindexesUT\Tux PKqE9E Querying.gdb/a00000003.gdbtableUT\Tux PKqE-  Querying.gdb/a00000003.gdbtablxUT\Tux PKqE,+Y1n Querying.gdb/a00000004.CatItemsByPhysicalName.atxUT\Tux PKqEeF8؞)2 Querying.gdb/a00000004.CatItemsByType.atxUT\Tux PKqE>;D#3 Querying.gdb/a00000004.FDO_UUID.atxUT\Tux PKqEWXq. Querying.gdb/a00000004.freelistUT\Tux PKqEӰ 6!`Querying.gdb/a00000004.gdbindexesUT\Tux PKqEY@(G>Querying.gdb/a00000004.gdbtableUT\Tux PKqEW1 Querying.gdb/a00000004.gdbtablxUT\Tux PKqE}LrpIQuerying.gdb/a00000004.spxUT\Tux PKqEczk1hQuerying.gdb/a00000005.CatRelsByDestinationID.atxUT\Tux PKqE Kc=U,>Querying.gdb/a00000005.CatRelsByOriginID.atxUT\Tux PKqEwES(Querying.gdb/a00000005.CatRelsByType.atxUT\Tux PKqEj#Querying.gdb/a00000005.FDO_UUID.atxUT\Tux PKqEr'0XuQuerying.gdb/a00000005.freelistUT\Tux PKqE>!Querying.gdb/a00000005.gdbindexesUT\Tux PKqEA٪Querying.gdb/a00000005.gdbtableUT\Tux PKqE:() NQuerying.gdb/a00000005.gdbtablxUT\Tux PKqE$5ЬQuerying.gdb/a00000006.CatRelTypesByBackwardLabel.atxUT\Tux PKqE|6cQuerying.gdb/a00000006.CatRelTypesByDestItemTypeID.atxUT\Tux PKqETU_4Querying.gdb/a00000006.CatRelTypesByForwardLabel.atxUT\Tux PKqE"',HQuerying.gdb/a00000006.CatRelTypesByName.atxUT\Tux PKqE/T8ղQuerying.gdb/a00000006.CatRelTypesByOriginItemTypeID.atxUT\Tux PKqE,Querying.gdb/a00000006.CatRelTypesByUUID.atxUT\Tux PKqEdZ!Querying.gdb/a00000006.gdbindexesUT\Tux PKqEFɗŷQuerying.gdb/a00000006.gdbtableUT\Tux PKqE銊S ׼Querying.gdb/a00000006.gdbtablxUT\Tux PKqEJN0-Querying.gdb/a00000007.CatItemTypesByName.atxUT\Tux PKqEöy5޿Querying.gdb/a00000007.CatItemTypesByParentTypeID.atxUT\Tux PKqE+]{2-cQuerying.gdb/a00000007.CatItemTypesByUUID.atxUT\Tux PKqEݦr(!Querying.gdb/a00000007.gdbindexesUT\Tux PKqEH 2Querying.gdb/a00000007.gdbtableUT\Tux PKqED} TQuerying.gdb/a00000007.gdbtablxUT\Tux PKqE[:gCt!*Querying.gdb/a00000009.gdbindexesUT\Tux PKqELG&Querying.gdb/a00000009.gdbtableUT\Tux PKqEQr ?Querying.gdb/a00000009.gdbtablxUT\Tux PKqE Querying.gdb/a00000009.spxUT\Tux PKqE{+B! Querying.gdb/a0000000a.gdbindexesUT\Tux PKqE^4l> Querying.gdb/a0000000a.gdbtableUT\Tux PKqE"ar (Querying.gdb/a0000000a.gdbtablxUT\Tux PKqE[:gCt!Querying.gdb/a0000000b.gdbindexesUT\Tux PKqEM/<Querying.gdb/a0000000b.gdbtableUT\Tux PKqE PQuerying.gdb/a0000000b.gdbtablxUT\Tux PKqEAjQuerying.gdb/a0000000b.spxUT\Tux PK qE./Querying.gdb/gdbUT\Tux PKqEs Querying.gdb/timestampsUT\Tux PK77@pgsql-ogr-fdw-1.1.5/data/enc.dbf000066400000000000000000000004471463733357200163640ustar00rootroot00000000000000_CnameC2ageNheightNbirthdateD Peter 45 5.6019650412 Pul 33 5.8419710325pgsql-ogr-fdw-1.1.5/data/enc.prj000066400000000000000000000002171463733357200164170ustar00rootroot00000000000000GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]pgsql-ogr-fdw-1.1.5/data/enc.shp000066400000000000000000000002341463733357200164150ustar00rootroot00000000000000' NTC֗^JF?,?? ,?? TC֗^JF?pgsql-ogr-fdw-1.1.5/data/enc.shx000066400000000000000000000001641463733357200164270ustar00rootroot00000000000000' :TC֗^JF?,??2 @ pgsql-ogr-fdw-1.1.5/data/natural.dbf000066400000000000000000000002211463733357200172530ustar00rootroot00000000000000_aIDNNATURALC 1298372893 wood 239287328 landpgsql-ogr-fdw-1.1.5/data/no_geom.csv000066400000000000000000000001061463733357200172720ustar00rootroot00000000000000name,age,value Peter,34,10.2 John,77,3.4 Paul,45,19.2 Matthew,35,18.2 pgsql-ogr-fdw-1.1.5/data/no_geom_apost.csv000066400000000000000000000001341463733357200205010ustar00rootroot00000000000000name,age,"person's value" Peter,34,10.2 John,77,3.4 Paul,45,19.2 Matthew,35,18.2 "",44,34.3 pgsql-ogr-fdw-1.1.5/data/poly.dbf000066400000000000000000000003231463733357200165730ustar00rootroot00000000000000_a&IDN NAMEC 1 One 2 Two 3 Threepgsql-ogr-fdw-1.1.5/data/poly.shp000066400000000000000000000014601463733357200166350ustar00rootroot00000000000000' fHb)PW ԿKZR?vļ!?pfHb)PW Կ~п88? fHy;Cb?袋..ogH? y;C88?袋.ؿ&PW ?~пlq?~@ݿM6d!1ogb)PW Կ!1ogb)PW Կ~@_vļͿ+jvļͿfHy;Cb?H`]tE?+jͿKZR?vļ!?lJZ?~@?KZR?vļ!?KZR?vļ!?NZR?lΐÿ`]tE?+jͿlJZ?~@?fHb)PW ԿKZR?vļ!? fHy;Cb?袋..ogH? y;C88?袋.ؿ&PW ?~пlq?~@ݿM6d!1ogb)PW Կ!1ogb)PW Կ~@_vļͿ+jvļͿfHy;Cb?lJZ?~@?KZR?vļ!?KZR?vļ!?NZR?lΐÿ`]tE?+jͿlJZ?~@?pgsql-ogr-fdw-1.1.5/data/poly.shx000066400000000000000000000001741463733357200166460ustar00rootroot00000000000000' >fHb)PW ԿKZR?vļ!?2pHpgsql-ogr-fdw-1.1.5/data/pt_two.dbf000066400000000000000000000004471463733357200171330ustar00rootroot00000000000000_CnameC2ageNheightNbirthdateD Peter 45 5.6019650412 Paul 33 5.8419710325pgsql-ogr-fdw-1.1.5/data/pt_two.prj000066400000000000000000000002171463733357200171660ustar00rootroot00000000000000GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]pgsql-ogr-fdw-1.1.5/data/pt_two.qpj000066400000000000000000000004011463733357200171600ustar00rootroot00000000000000GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] pgsql-ogr-fdw-1.1.5/data/pt_two.shp000066400000000000000000000002341463733357200171640ustar00rootroot00000000000000' NTC֗^JF?,?? ,?? TC֗^JF?pgsql-ogr-fdw-1.1.5/data/pt_two.shx000066400000000000000000000001641463733357200171760ustar00rootroot00000000000000' :TC֗^JF?,??2 @ pgsql-ogr-fdw-1.1.5/expected/000077500000000000000000000000001463733357200160255ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/expected/.gitignore000066400000000000000000000001071463733357200200130ustar00rootroot00000000000000# Ignore everything in this directory * # Except this file !.gitignore pgsql-ogr-fdw-1.1.5/input/000077500000000000000000000000001463733357200153635ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/input/file.source000066400000000000000000000072751463733357200175370ustar00rootroot00000000000000 CREATE EXTENSION ogr_fdw; CREATE SERVER myserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile' ); ------------------------------------------------ CREATE FOREIGN TABLE pt_1 ( fid integer, geom bytea, name varchar, age integer, height double precision, birthdate date ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_1 WHERE fid = 1; ------------------------------------------------ CREATE FOREIGN TABLE pt_2 ( fid integer, name varchar ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_2 ORDER BY name; ------------------------------------------------ CREATE FOREIGN TABLE pt_3 ( geom bytea, name varchar ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_3 ORDER BY name; ------------------------------------------------ CREATE FOREIGN TABLE poly_1 ( fid bigint, geom bytea, id double precision, name varchar(5) ) SERVER myserver OPTIONS (layer 'poly'); SELECT length(geom) FROM poly_1 WHERE name = 'Three'; ------------------------------------------------ -- Laundering and explicit column naming test CREATE FOREIGN TABLE column_name_test ( fid integer, name varchar OPTIONS (column_name '2ame'), theage integer OPTIONS (column_name 'age'), height real OPTIONS (column_name 'Height'), birthdate date OPTIONS (column_name 'b-rthdate') ) SERVER myserver OPTIONS (layer '2launder'); SELECT * FROM column_name_test ORDER BY fid; -- Check that columns are reverse-laundered when generating -- OGR SQL filters SET client_min_messages = debug1; SELECT name FROM column_name_test WHERE name = 'Paul'; SET client_min_messages = notice; ------------------------------------------------ -- GDAL options passing tests CREATE SERVER myserver_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile', config_options 'SHAPE_ENCODING=LATIN1' ); CREATE FOREIGN TABLE e_1 ( fid integer, name varchar ) SERVER myserver_latin1 OPTIONS ( layer 'enc' ); SET client_min_messages = debug1; SELECT fid, name FROM e_1 WHERE fid = 1; SET client_min_messages = notice; ------------------------------------------------ -- Using encoding option directly CREATE SERVER myserver_latin1_direct FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile', character_encoding 'LATIN1' ); CREATE FOREIGN TABLE e_2 ( fid integer, name varchar ) SERVER myserver_latin1_direct OPTIONS ( layer 'enc' ); SELECT fid, name FROM e_2 WHERE fid = 1; ------------------------------------------------ -- Geometryless test CREATE SERVER csvserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data/no_geom.csv', format 'CSV' ); CREATE FOREIGN TABLE no_geom ( fid bigint, name varchar, age varchar, value varchar ) SERVER csvserver OPTIONS (layer 'no_geom'); SELECT c.* FROM generate_series(1,4) g JOIN no_geom c ON (c.fid = g.g); ------------------------------------------------ -- FGDB test CREATE SERVER fgdbserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/vsizip/@abs_srcdir@/data/Querying.zip/Querying.gdb', format 'OpenFileGDB' ); CREATE FOREIGN TABLE cities ( fid bigint, shape bytea, city_fips varchar(5), city_name varchar(40), state_fips varchar(2), state_name varchar(25), state_city varchar(7), type varchar(25), capital varchar(1), elevation integer, pop1990 integer, popcat integer ) SERVER "fgdbserver" OPTIONS (layer 'Cities'); SET client_min_messages = LOG; SELECT fid, city_name, pop1990 FROM cities WHERE pop1990 = 17710; SELECT fid, city_name, pop1990 FROM cities WHERE city_name = 'Williston'; pgsql-ogr-fdw-1.1.5/input/import.source000066400000000000000000000040021463733357200201130ustar00rootroot00000000000000set client_min_messages=NOTICE; ------------------------------------------------ CREATE SCHEMA imp1; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO (n2launder) FROM SERVER myserver INTO imp1; SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp1' and fc.relname = 'n2launder' ORDER BY c.ordinal_position; SELECT * FROM imp1.n2launder WHERE fid = 0; ------------------------------------------------ CREATE SCHEMA imp2; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO ("natural") FROM SERVER myserver INTO imp2; SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp2' and fc.relname = 'natural' ORDER BY c.ordinal_position; SELECT "natural" FROM imp2."natural"; ------------------------------------------------ CREATE SERVER svr_test_apost FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data/no_geom_apost.csv', format 'CSV' ); CREATE SCHEMA imp3; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO (no_geom_apost) FROM SERVER svr_test_apost INTO imp3; SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp3' and fc.relname = 'no_geom_apost' ORDER BY c.ordinal_position; SELECT name,age FROM imp3.no_geom_apost WHERE name = 'Paul'; SELECT name,age FROM imp3.no_geom_apost WHERE name IS NULL; SELECT name,age FROM imp3.no_geom_apost WHERE name = ''; ------------------------------------------------ pgsql-ogr-fdw-1.1.5/input/pgsql.source000066400000000000000000000106271463733357200177410ustar00rootroot00000000000000---------------------------------------------------------------------- -- Create local table CREATE TABLE bytea_local ( fid serial primary key, geom bytea, name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ); ---------------------------------------------------------------------- -- Populate local table INSERT INTO bytea_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Jim', '14232'::bytea, 23, 1, 4.3, 5.5, '2010-10-10'::date, '13:23:21'::time, '2010-10-10 13:23:21'::timestamp, 'this', 'y' ), ('Marvin', '55555'::bytea, 34, 2, 5.4, 10.13, '2011-11-11'::date, '15:21:45'::time, '2011-11-11 15:21:45'::timestamp, 'that', 'n' ), (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote table CREATE SERVER pgserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'PG:dbname=contrib_regression host=localhost', format 'PostgreSQL' ); CREATE FOREIGN TABLE bytea_fdw ( fid bigint, geom bytea, name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ) SERVER pgserver OPTIONS (layer 'bytea_local'); SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw; SELECT a.name, b.name FROM bytea_local a JOIN bytea_fdw b USING (fid); EXPLAIN VERBOSE SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw; ---------------------------------------------------------------------- -- Remote Query and OGR SQL pushdown SET client_min_messages = DEBUG1; SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw WHERE fid = 4; SELECT fid, name, dt FROM bytea_fdw WHERE name IS NULL; SELECT fid, name FROM bytea_fdw WHERE name = 'Jim' AND age <= 30; SELECT fid, name, dt FROM bytea_fdw WHERE name = 'Jim' AND age <= 30 AND dt > '2010-10-1'::date; SELECT fid, name FROM bytea_fdw WHERE name = 'Jim' OR name IS NULL; ---------------------------------------------------------------------- -- Cached query case, exercised by statement handles or -- functions. CREATE OR REPLACE FUNCTION get_names() RETURNS varchar AS $$ BEGIN RETURN (SELECT string_agg(name,',') FROM bytea_fdw WHERE name = 'Jim' OR name IS NULL); END; $$ LANGUAGE 'plpgsql'; SELECT get_names(); DROP FUNCTION get_names(); ---------------------------------------------------------------------- -- Remote Update INSERT INTO bytea_fdw (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Margaret', '2222'::bytea, 12, 5, 1.4, 19.13, '2001-11-23'::date, '9:12:34'::time, '2001-02-11 09:23:11'::timestamp, 'them', 'y' ) RETURNING fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn; UPDATE bytea_fdw SET name = 'Maggie', num = 45.34, yn = 'n' WHERE age = 12; SELECT fid, name, num, yn FROM bytea_fdw WHERE fid = 4; UPDATE bytea_fdw SET dt = '2089-12-13', tm = '01:23:45' WHERE num = 45.34; SELECT fid, dt, tm FROM bytea_fdw WHERE fid = 4; DELETE FROM bytea_fdw WHERE fid = 4; SELECT a.fid, a.name, b.name FROM bytea_local a JOIN bytea_fdw b USING (fid); ---------------------------------------------------------------------- -- Populate local array table SET client_min_messages = NOTICE; CREATE TABLE array_local ( fid integer primary key, geom bytea, txt text[], int int2[], flt float4[], b boolean[] ); INSERT INTO array_local (fid,txt, int, flt, b) VALUES (1, ARRAY['Jim'], ARRAY[1,2,3], ARRAY[3.4,5.6,7.8], ARRAY[true,false]), (2, ARRAY['Jim',NULL,'Joe'], ARRAY[1,3,NULL,4], ARRAY[4.5,NULL,3.4], ARRAY[false,NULL]), (3, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote array table CREATE FOREIGN TABLE array_fdw ( fid bigint, geom bytea, txt text[], int int4[], flt float8[], b boolean[] ) SERVER pgserver OPTIONS (layer 'array_local'); SELECT fid, txt, int, flt, b FROM array_fdw; ---------------------------------------------------------------------- -- Update remote array table UPDATE array_fdw SET txt = ARRAY['newJim', 'newJoe'], int = ARRAY[-2, -1, 0, 1, 2], flt = ARRAY[-0.1, 0.0, 0.1] WHERE fid = 3; SELECT txt, int, flt FROM array_fdw WHERE fid = 3; pgsql-ogr-fdw-1.1.5/input/postgis.source000066400000000000000000000050571463733357200203040ustar00rootroot00000000000000CREATE EXTENSION postgis; CREATE TABLE geometry_local ( fid serial primary key, geom geometry(Point, 4326), name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ); ---------------------------------------------------------------------- -- Populate local table INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Jim', 'SRID=4326;POINT(0 0)', 23, 1, 4.3, 5.5, '2010-10-10'::date, '13:23:21'::time, '2010-10-10 13:23:21'::timestamp, 'this', 'y' ); INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Marvin', 'SRID=4326;POINT(100 0)', 34, 2, 5.4, 10.13, '2011-11-11'::date, '15:21:45'::time, '2011-11-11 15:21:45'::timestamp, 'that', 'n' ); INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote table CREATE SERVER pgservergeom FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'PG:dbname=contrib_regression host=localhost', format 'PostgreSQL' ); CREATE FOREIGN TABLE geometry_fdw ( fid integer, geom geometry(point, 4326), name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ) SERVER pgservergeom OPTIONS (layer 'geometry_local'); SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM geometry_fdw; SELECT a.name, b.name FROM geometry_local a JOIN geometry_fdw b USING (fid); EXPLAIN VERBOSE SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM geometry_fdw; ---------------------------------------------------------------------- -- Remote Query and OGR SQL pushdown SET client_min_messages = DEBUG1; SELECT name, age, ST_AsText(geom) FROM geometry_fdw WHERE name = 'Jim' AND age <= 30 AND geom && ST_MakeEnvelope(-1, -1, 1, 1, 4326); SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_Intersects(geom, ST_MakeEnvelope(-1, -1, 1, 1, 4326)); SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE geom && ST_MakeEnvelope(-180, -90, 180, 90, 4326); SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_MakeEnvelope(-180, -90, 180, 90, 4326) && geom; SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_MakeEnvelope(-180, -90, 180, 90, 4326) && ST_MakeEnvelope(-180, -90, 180, 90, 4326); pgsql-ogr-fdw-1.1.5/ogr_fdw--1.0--1.1.sql000066400000000000000000000004531463733357200173210ustar00rootroot00000000000000CREATE OR REPLACE FUNCTION ogr_fdw_version() RETURNS text AS 'MODULE_PATHNAME', 'ogr_fdw_version' LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION ogr_fdw_drivers() RETURNS text[] AS 'MODULE_PATHNAME', 'ogr_fdw_drivers' LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE; pgsql-ogr-fdw-1.1.5/ogr_fdw--1.1.sql000066400000000000000000000013771463733357200167560ustar00rootroot00000000000000/* ogr_fdw/ogr_fdw--1.1.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION ogr_fdw" to load this file. \quit CREATE FUNCTION ogr_fdw_handler() RETURNS fdw_handler AS 'MODULE_PATHNAME' LANGUAGE 'c' STRICT; CREATE FUNCTION ogr_fdw_validator(text[], oid) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE 'c' STRICT; CREATE FOREIGN DATA WRAPPER ogr_fdw HANDLER ogr_fdw_handler VALIDATOR ogr_fdw_validator; CREATE OR REPLACE FUNCTION ogr_fdw_version() RETURNS text AS 'MODULE_PATHNAME', 'ogr_fdw_version' LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION ogr_fdw_drivers() RETURNS text[] AS 'MODULE_PATHNAME', 'ogr_fdw_drivers' LANGUAGE 'c' IMMUTABLE STRICT PARALLEL SAFE; pgsql-ogr-fdw-1.1.5/ogr_fdw.c000066400000000000000000002564761463733357200160430ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw.c * foreign-data wrapper for GIS data access. * * Copyright (c) 2014-2015, Paul Ramsey * *------------------------------------------------------------------------- */ /* * PostgreSQL */ #include "postgres.h" /* * System */ #include #include /* * Require PostgreSQL >= 9.3 */ #if PG_VERSION_NUM < 90300 #error "OGR FDW requires PostgreSQL version 9.3 or higher" #else /* * Definition of stringToQualifiedNameList */ #if PG_VERSION_NUM >= 100000 #include "utils/regproc.h" #endif /* * Local structures */ #include "ogr_fdw.h" PG_MODULE_MAGIC; /* * Describes the valid options for objects that use this wrapper. */ struct OgrFdwOption { const char* optname; Oid optcontext; /* Oid of catalog in which option may appear */ bool optrequired; /* Flag mandatory options */ bool optfound; /* Flag whether options was specified by user */ }; #define OPT_DRIVER "format" #define OPT_SOURCE "datasource" #define OPT_LAYER "layer" #define OPT_COLUMN "column_name" #define OPT_CONFIG_OPTIONS "config_options" #define OPT_OPEN_OPTIONS "open_options" #define OPT_UPDATEABLE "updateable" #define OPT_CHAR_ENCODING "character_encoding" #define OGR_FDW_FRMT_INT64 "%lld" #define OGR_FDW_CAST_INT64(x) (long long)(x) /* * Valid options for ogr_fdw. * ForeignDataWrapperRelationId (no options) * ForeignServerRelationId (CREATE SERVER options) * UserMappingRelationId (CREATE USER MAPPING options) * ForeignTableRelationId (CREATE FOREIGN TABLE options) * * {optname, optcontext, optrequired, optfound} */ static struct OgrFdwOption valid_options[] = { /* OGR column mapping */ {OPT_COLUMN, AttributeRelationId, false, false}, /* OGR datasource options */ {OPT_SOURCE, ForeignServerRelationId, true, false}, {OPT_DRIVER, ForeignServerRelationId, false, false}, {OPT_UPDATEABLE, ForeignServerRelationId, false, false}, {OPT_CONFIG_OPTIONS, ForeignServerRelationId, false, false}, {OPT_CHAR_ENCODING, ForeignServerRelationId, false, false}, #if GDAL_VERSION_MAJOR >= 2 {OPT_OPEN_OPTIONS, ForeignServerRelationId, false, false}, #endif /* OGR layer options */ {OPT_LAYER, ForeignTableRelationId, true, false}, {OPT_UPDATEABLE, ForeignTableRelationId, false, false}, /* EOList marker */ {NULL, InvalidOid, false, false} }; /* * SQL functions */ extern Datum ogr_fdw_handler(PG_FUNCTION_ARGS); extern Datum ogr_fdw_validator(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(ogr_fdw_handler); PG_FUNCTION_INFO_V1(ogr_fdw_validator); void _PG_init(void); /* * FDW query callback routines */ static void ogrGetForeignRelSize(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid); static void ogrGetForeignPaths(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid); static ForeignScan* ogrGetForeignPlan(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid, ForeignPath* best_path, List* tlist, List* scan_clauses #if PG_VERSION_NUM >= 90500 , Plan* outer_plan #endif ); static void ogrBeginForeignScan(ForeignScanState* node, int eflags); static TupleTableSlot* ogrIterateForeignScan(ForeignScanState* node); static void ogrReScanForeignScan(ForeignScanState* node); static void ogrEndForeignScan(ForeignScanState* node); /* * FDW modify callback routines */ #if PG_VERSION_NUM >= 140000 static void ogrAddForeignUpdateTargets(PlannerInfo* planinfo, unsigned int rte_index, RangeTblEntry* target_rte, Relation target_relation); #else static void ogrAddForeignUpdateTargets(Query* parsetree, RangeTblEntry* target_rte, Relation target_relation); #endif static void ogrBeginForeignModify(ModifyTableState* mtstate, ResultRelInfo* rinfo, List* fdw_private, int subplan_index, int eflags); static TupleTableSlot* ogrExecForeignInsert(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot); static TupleTableSlot* ogrExecForeignUpdate(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot); static TupleTableSlot* ogrExecForeignDelete(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot); static void ogrEndForeignModify(EState* estate, ResultRelInfo* rinfo); static int ogrIsForeignRelUpdatable(Relation rel); #if PG_VERSION_NUM >= 90500 static List* ogrImportForeignSchema(ImportForeignSchemaStmt* stmt, Oid serverOid); #endif /* * Helper functions */ static OgrConnection ogrGetConnectionFromTable(Oid foreigntableid, OgrUpdateable updateable); static void ogr_fdw_exit(int code, Datum arg); static void ogrReadColumnData(OgrFdwState* state); /* Global to hold GEOMETRYOID */ Oid GEOMETRYOID = InvalidOid; #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,1,0) const char* const gdalErrorTypes[] = { "None", "AppDefined", "OutOfMemory", "FileIO", "OpenFailed", "IllegalArg", "NotSupported", "AssertionFailed", "NoWriteAccess", "UserInterrupt", "ObjectNull", "HttpResponse", "AWSBucketNotFound", "AWSObjectNotFound", "AWSAccessDenied", "AWSInvalidCredentials", "AWSSignatureDoesNotMatch" }; /* In theory this function should be declared "static void CPL_STDCALL" */ /* since this is the official signature of error handler callbacks. */ /* That would be needed if both GDAL and ogr_fdw were compiled with Visual */ /* Studio, but with non-Visual Studio compilers, the macro expands to empty, */ /* so if both GDAL and ogr_fdw are compiled with gcc things are fine. In case */ /* of mixes, crashes may occur but there is no clean fix... So let this as a note */ /* in case of future issue... */ static void ogrErrorHandler(CPLErr eErrClass, int err_no, const char* msg) { const char* gdalErrType = "unknown type"; if (err_no >= 0 && err_no < (int)sizeof(gdalErrorTypes) / sizeof(gdalErrorTypes[0])) { gdalErrType = gdalErrorTypes[err_no]; } switch (eErrClass) { case CE_None: elog(NOTICE, "GDAL %s [%d] %s", gdalErrType, err_no, msg); break; case CE_Debug: elog(DEBUG2, "GDAL %s [%d] %s", gdalErrType, err_no, msg); break; case CE_Warning: elog(WARNING, "GDAL %s [%d] %s", gdalErrType, err_no, msg); break; case CE_Failure: case CE_Fatal: default: elog(ERROR, "GDAL %s [%d] %s", gdalErrType, err_no, msg); break; } return; } #endif /* GDAL 2.1.0+ */ void _PG_init(void) { on_proc_exit(&ogr_fdw_exit, PointerGetDatum(NULL)); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,1,0) /* Hook up the GDAL error handlers to PgSQL elog() */ CPLSetErrorHandler(ogrErrorHandler); CPLSetCurrentErrorHandlerCatchDebug(true); #endif } /* * ogr_fdw_exit: Exit callback function. */ static void ogr_fdw_exit(int code, Datum arg) { OGRCleanupAll(); } /* * Function to get the geometry OID if required */ Oid ogrGetGeometryOid(void) { if (GEOMETRYOID == InvalidOid) { Oid typoid = TypenameGetTypid("geometry"); if (OidIsValid(typoid) && get_typisdefined(typoid)) { GEOMETRYOID = typoid; } else { GEOMETRYOID = BYTEAOID; } } return GEOMETRYOID; } /* * Foreign-data wrapper handler function: return a struct with pointers * to my callback routines. */ Datum ogr_fdw_handler(PG_FUNCTION_ARGS) { FdwRoutine* fdwroutine = makeNode(FdwRoutine); /* Read support */ fdwroutine->GetForeignRelSize = ogrGetForeignRelSize; fdwroutine->GetForeignPaths = ogrGetForeignPaths; fdwroutine->GetForeignPlan = ogrGetForeignPlan; fdwroutine->BeginForeignScan = ogrBeginForeignScan; fdwroutine->IterateForeignScan = ogrIterateForeignScan; fdwroutine->ReScanForeignScan = ogrReScanForeignScan; fdwroutine->EndForeignScan = ogrEndForeignScan; /* Write support */ fdwroutine->AddForeignUpdateTargets = ogrAddForeignUpdateTargets; fdwroutine->BeginForeignModify = ogrBeginForeignModify; fdwroutine->ExecForeignInsert = ogrExecForeignInsert; fdwroutine->ExecForeignUpdate = ogrExecForeignUpdate; fdwroutine->ExecForeignDelete = ogrExecForeignDelete; fdwroutine->EndForeignModify = ogrEndForeignModify; fdwroutine->IsForeignRelUpdatable = ogrIsForeignRelUpdatable; #if PG_VERSION_NUM >= 90500 /* Support functions for IMPORT FOREIGN SCHEMA */ fdwroutine->ImportForeignSchema = ogrImportForeignSchema; #endif PG_RETURN_POINTER(fdwroutine); } /* * When attempting a soft open (allowing for failure and retry), * we might need to call the opening * routines twice, so all the opening machinery is placed here * for convenient re-calling. */ static OGRErr ogrGetDataSourceAttempt(OgrConnection* ogr, bool bUpdateable, char** open_option_list) { GDALDriverH ogr_dr = NULL; #if GDAL_VERSION_MAJOR >= 2 unsigned int open_flags = GDAL_OF_VECTOR; if (bUpdateable) { open_flags |= GDAL_OF_UPDATE; } else { open_flags |= GDAL_OF_READONLY; } #endif if (ogr->dr_str) { ogr_dr = GDALGetDriverByName(ogr->dr_str); if (!ogr_dr) { ereport(ERROR, (errcode(ERRCODE_FDW_UNABLE_TO_ESTABLISH_CONNECTION), errmsg("unable to find format \"%s\"", ogr->dr_str), errhint("See the formats list at http://www.gdal.org/ogr_formats.html"))); } #if GDAL_VERSION_MAJOR < 2 ogr->ds = OGR_Dr_Open(ogr_dr, ogr->ds_str, bUpdateable); #else { char** driver_list = CSLAddString(NULL, ogr->dr_str); ogr->ds = GDALOpenEx(ogr->ds_str, /* file/data source */ open_flags, /* open flags */ (const char* const*)driver_list, /* driver */ (const char* const*)open_option_list, /* open options */ NULL); /* sibling files */ CSLDestroy(driver_list); } #endif } /* No driver, try a blind open... */ else { #if GDAL_VERSION_MAJOR < 2 ogr->ds = OGROpen(ogr->ds_str, bUpdateable, &ogr_dr); #else ogr->ds = GDALOpenEx(ogr->ds_str, open_flags, NULL, (const char* const*)open_option_list, NULL); #endif } return ogr->ds ? OGRERR_NONE : OGRERR_FAILURE; } /* * Given a connection string and (optional) driver string, try to connect * with appropriate error handling and reporting. Used in query startup, * and in FDW options validation. */ static OGRErr ogrGetDataSource(OgrConnection* ogr, OgrUpdateable updateable) { char** open_option_list = NULL; bool bUpdateable = (updateable == OGR_UPDATEABLE_TRUE || updateable == OGR_UPDATEABLE_TRY); OGRErr err; /* Set the GDAL config options into the environment */ if (ogr->config_options) { char** option_iter; char** option_list = CSLTokenizeString(ogr->config_options); for (option_iter = option_list; option_iter && *option_iter; option_iter++) { char* key; const char* value; value = CPLParseNameValue(*option_iter, &key); if (!(key && value)) { elog(ERROR, "bad config option string '%s'", ogr->config_options); } elog(DEBUG1, "GDAL config option '%s' set to '%s'", key, value); CPLSetConfigOption(key, value); CPLFree(key); } CSLDestroy(option_list); } /* Parse the GDAL layer open options */ if (ogr->open_options) { open_option_list = CSLTokenizeString(ogr->open_options); } /* Cannot search for drivers if they aren't registered, */ /* but don't do registration if we already have drivers loaded */ if (GDALGetDriverCount() <= 0) { GDALAllRegister(); } /* First attempt at connection */ err = ogrGetDataSourceAttempt(ogr, bUpdateable, open_option_list); /* Failed on soft updateable attempt, try and fall back to readonly */ if ((!ogr->ds) && updateable == OGR_UPDATEABLE_TRY) { err = ogrGetDataSourceAttempt(ogr, false, open_option_list); /* Succeeded with readonly connection */ if (ogr->ds) { ogr->ds_updateable = ogr->lyr_updateable = OGR_UPDATEABLE_FALSE; } } /* Open failed, provide error hint if OGR gives us one. */ if (!ogr->ds) { const char* ogrerrmsg = CPLGetLastErrorMsg(); if (ogrerrmsg && !streq(ogrerrmsg, "")) { ereport(ERROR, (errcode(ERRCODE_FDW_UNABLE_TO_ESTABLISH_CONNECTION), errmsg("unable to connect to data source \"%s\"", ogr->ds_str), errhint("%s", ogrerrmsg))); } else { ereport(ERROR, (errcode(ERRCODE_FDW_UNABLE_TO_ESTABLISH_CONNECTION), errmsg("unable to connect to data source \"%s\"", ogr->ds_str))); } } CSLDestroy(open_option_list); return err; } static bool ogrCanReallyCountFast(const OgrConnection* con) { GDALDriverH dr = GDALGetDatasetDriver(con->ds); const char* dr_str = GDALGetDriverShortName(dr); if (streq(dr_str, "ESRI Shapefile") || streq(dr_str, "FileGDB") || streq(dr_str, "OpenFileGDB")) { return true; } return false; } static void ogrEreportError(const char* errstr) { const char* ogrerrmsg = CPLGetLastErrorMsg(); if (ogrerrmsg && !streq(ogrerrmsg, "")) { ereport(ERROR, (errcode(ERRCODE_FDW_ERROR), errmsg("%s", errstr), errhint("%s", ogrerrmsg))); } else { ereport(ERROR, (errcode(ERRCODE_FDW_ERROR), errmsg("%s", errstr))); } } /* * Make sure the datasource is cleaned up when we're done * with a connection. */ static void ogrFinishConnection(OgrConnection* ogr) { elog(DEBUG3, "%s: entered function", __func__); if (ogr->lyr && OGR_L_SyncToDisk(ogr->lyr) != OGRERR_NONE) { elog(NOTICE, "failed to flush writes to OGR data source"); } if (ogr->ds) { GDALClose(ogr->ds); } ogr->ds = NULL; } static OgrConnection ogrGetConnectionFromServer(Oid foreignserverid, OgrUpdateable updateable) { ForeignServer* server; OgrConnection ogr; ListCell* cell; OGRErr err; elog(DEBUG3, "%s: entered function", __func__); /* Null all values */ memset(&ogr, 0, sizeof(OgrConnection)); ogr.ds_updateable = OGR_UPDATEABLE_UNSET; ogr.lyr_updateable = OGR_UPDATEABLE_UNSET; server = GetForeignServer(foreignserverid); foreach (cell, server->options) { DefElem* def = (DefElem*) lfirst(cell); if (streq(def->defname, OPT_SOURCE)) { ogr.ds_str = defGetString(def); } if (streq(def->defname, OPT_DRIVER)) { ogr.dr_str = defGetString(def); } if (streq(def->defname, OPT_CONFIG_OPTIONS)) { ogr.config_options = defGetString(def); } if (streq(def->defname, OPT_OPEN_OPTIONS)) { ogr.open_options = defGetString(def); } if (streq(def->defname, OPT_CHAR_ENCODING)) { ogr.char_encoding = pg_char_to_encoding(defGetString(def)); } if (streq(def->defname, OPT_UPDATEABLE)) { if (defGetBoolean(def)) { ogr.ds_updateable = OGR_UPDATEABLE_TRUE; } else { ogr.ds_updateable = OGR_UPDATEABLE_FALSE; /* Over-ride the open mode to favour user-defined mode */ updateable = OGR_UPDATEABLE_FALSE; } } } if (!ogr.ds_str) { elog(ERROR, "FDW table '%s' option is missing", OPT_SOURCE); } /* * TODO: Connections happen twice for each query, having a * connection pool will certainly make things faster. */ /* Connect! */ err = ogrGetDataSource(&ogr, updateable); if (err == OGRERR_FAILURE) { elog(ERROR, "ogrGetDataSource failed"); } return ogr; } /* * Read the options (data source connection from server and * layer name from table) from a foreign table and use them * to connect to an OGR layer. Return a connection object that * has handles for both the datasource and layer. */ static OgrConnection ogrGetConnectionFromTable(Oid foreigntableid, OgrUpdateable updateable) { ForeignTable* table; /* UserMapping *mapping; */ /* ForeignDataWrapper *wrapper; */ ListCell* cell; OgrConnection ogr; elog(DEBUG3, "%s: entered function", __func__); /* Gather all data for the foreign table. */ table = GetForeignTable(foreigntableid); /* mapping = GetUserMapping(GetUserId(), table->serverid); */ ogr = ogrGetConnectionFromServer(table->serverid, updateable); elog(DEBUG3, "%s: ogr.ds_str = %s", __func__, ogr.ds_str); foreach (cell, table->options) { DefElem* def = (DefElem*) lfirst(cell); if (streq(def->defname, OPT_LAYER)) { ogr.lyr_str = defGetString(def); } if (streq(def->defname, OPT_UPDATEABLE)) { if (defGetBoolean(def)) { if (ogr.ds_updateable == OGR_UPDATEABLE_FALSE) { ereport(ERROR, ( errcode(ERRCODE_FDW_ERROR), errmsg("data source \"%s\" is not updateable", ogr.ds_str), errhint("cannot set table '%s' option to true", OPT_UPDATEABLE) )); } ogr.lyr_updateable = OGR_UPDATEABLE_TRUE; } else { ogr.lyr_updateable = OGR_UPDATEABLE_FALSE; } } } if (!ogr.lyr_str) { elog(ERROR, "FDW table '%s' option is missing", OPT_LAYER); } elog(DEBUG3, "%s: ogr.lyr_str = %s", __func__, ogr.lyr_str); /* Does the layer exist in the data source? */ ogr.lyr = GDALDatasetGetLayerByName(ogr.ds, ogr.lyr_str); if (!ogr.lyr) { const char* ogrerr = CPLGetLastErrorMsg(); ereport(ERROR, ( errcode(ERRCODE_FDW_TABLE_NOT_FOUND), errmsg("unable to connect to %s to \"%s\"", OPT_LAYER, ogr.lyr_str), (ogrerr && ! streq(ogrerr, "")) ? errhint("%s", ogrerr) : errhint("Does the layer exist?") )); } if (OGR_L_TestCapability(ogr.lyr, OLCStringsAsUTF8)) { ogr.char_encoding = PG_UTF8; } return ogr; } /* * Validate the options given to a FOREIGN DATA WRAPPER, SERVER, * USER MAPPING or FOREIGN TABLE that uses ogr_fdw. * * Raise an ERROR if the option or its value is considered invalid. */ Datum ogr_fdw_validator(PG_FUNCTION_ARGS) { List* options_list = untransformRelOptions(PG_GETARG_DATUM(0)); Oid catalog = PG_GETARG_OID(1); ListCell* cell; struct OgrFdwOption* opt; const char* source = NULL, *driver = NULL; const char* config_options = NULL, *open_options = NULL; OgrUpdateable updateable = OGR_UPDATEABLE_FALSE; /* Initialize found state to not found */ for (opt = valid_options; opt->optname; opt++) { opt->optfound = false; } /* * Check that only options supported by ogr_fdw, and allowed for the * current object type, are given. */ foreach (cell, options_list) { DefElem* def = (DefElem*) lfirst(cell); bool optfound = false; for (opt = valid_options; opt->optname; opt++) { if (catalog == opt->optcontext && streq(opt->optname, def->defname)) { /* Mark that this user option was found */ opt->optfound = optfound = true; /* Store some options for testing later */ if (streq(opt->optname, OPT_SOURCE)) { source = defGetString(def); } if (streq(opt->optname, OPT_DRIVER)) { driver = defGetString(def); } if (streq(opt->optname, OPT_CONFIG_OPTIONS)) { config_options = defGetString(def); } if (streq(opt->optname, OPT_OPEN_OPTIONS)) { open_options = defGetString(def); } if (streq(opt->optname, OPT_UPDATEABLE)) { if (defGetBoolean(def)) { updateable = OGR_UPDATEABLE_TRY; } } break; } } if (!optfound) { /* * Unknown option specified, complain about it. Provide a hint * with list of valid options for the object. */ const struct OgrFdwOption* opt; StringInfoData buf; initStringInfo(&buf); for (opt = valid_options; opt->optname; opt++) { if (catalog == opt->optcontext) appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "", opt->optname); } ereport(ERROR, ( errcode(ERRCODE_FDW_INVALID_OPTION_NAME), errmsg("invalid option \"%s\"", def->defname), buf.len > 0 ? errhint("Valid options in this context are: %s", buf.data) : errhint("There are no valid options in this context."))); } } /* Check that all the mandatory options were found */ for (opt = valid_options; opt->optname; opt++) { /* Required option for this catalog type is missing? */ if (catalog == opt->optcontext && opt->optrequired && ! opt->optfound) { ereport(ERROR, ( errcode(ERRCODE_FDW_DYNAMIC_PARAMETER_VALUE_NEEDED), errmsg("required option \"%s\" is missing", opt->optname))); } } /* Make sure server connection can actually be established */ if (catalog == ForeignServerRelationId && source) { OgrConnection ogr; OGRErr err; ogr.ds_str = source; ogr.dr_str = driver; ogr.config_options = config_options; ogr.open_options = open_options; err = ogrGetDataSource(&ogr, updateable); if (err == OGRERR_FAILURE) { elog(ERROR, "ogrGetDataSource failed"); } if (ogr.ds) { GDALClose(ogr.ds); } } PG_RETURN_VOID(); } /* * Initialize an OgrFdwPlanState on the heap. */ static OgrFdwState* getOgrFdwState(Oid foreigntableid, OgrFdwStateType state_type) { OgrFdwState* state; size_t size; OgrUpdateable updateable = OGR_UPDATEABLE_FALSE; switch (state_type) { case OGR_PLAN_STATE: size = sizeof(OgrFdwPlanState); updateable = OGR_UPDATEABLE_FALSE; break; case OGR_EXEC_STATE: size = sizeof(OgrFdwExecState); updateable = OGR_UPDATEABLE_FALSE; break; case OGR_MODIFY_STATE: updateable = OGR_UPDATEABLE_TRUE; size = sizeof(OgrFdwModifyState); break; default: elog(ERROR, "invalid state type"); } state = palloc0(size); state->type = state_type; /* Connect! */ state->ogr = ogrGetConnectionFromTable(foreigntableid, updateable); state->foreigntableid = foreigntableid; return state; } /* * ogrGetForeignRelSize * Obtain relation size estimates for a foreign table */ static void ogrGetForeignRelSize(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid) { /* Initialize the OGR connection */ OgrFdwState* state = (OgrFdwState*)getOgrFdwState(foreigntableid, OGR_PLAN_STATE); OgrFdwPlanState* planstate = (OgrFdwPlanState*)state; List* scan_clauses = baserel->baserestrictinfo; elog(DEBUG3, "%s: entered function", __func__); /* Set to NULL to clear the restriction clauses in OGR */ OGR_L_SetIgnoredFields(planstate->ogr.lyr, NULL); OGR_L_SetSpatialFilter(planstate->ogr.lyr, NULL); OGR_L_SetAttributeFilter(planstate->ogr.lyr, NULL); /* * The estimate number of rows returned must actually use restrictions. * Since OGR can't really give us a fast count with restrictions on * (usually involves a scan) and restrictions in the baserel mean we * must punt row count estimates. */ /* TODO: calculate the row width based on the attribute types of the OGR table */ /* * OGR asks drivers to honestly state if they can provide a fast * row count, but too many drivers lie. We are only listing drivers * we trust in ogrCanReallyCountFast() */ /* If we can quickly figure how many rows this layer has, then do so */ if (scan_clauses == NIL && OGR_L_TestCapability(planstate->ogr.lyr, OLCFastFeatureCount) == TRUE && ogrCanReallyCountFast(&(planstate->ogr))) { /* Count rows, but don't force a slow count */ int rows = OGR_L_GetFeatureCount(planstate->ogr.lyr, false); /* Only use row count if return is valid (>0) */ if (rows >= 0) { planstate->nrows = rows; baserel->rows = rows; } } /* Save connection state for next calls */ baserel->fdw_private = (void*) planstate; return; } /* * ogrGetForeignPaths * Create possible access paths for a scan on the foreign table * * Currently there is only one * possible access path, which simply returns all records in the order in * the data file. */ static void ogrGetForeignPaths(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid) { OgrFdwPlanState* planstate = (OgrFdwPlanState*)(baserel->fdw_private); elog(DEBUG3, "%s: entered function", __func__); /* TODO: replace this with something that looks at the OGRDriver and */ /* makes a determination based on that? Better: add connection caching */ /* so that slow startup doesn't matter so much */ planstate->startup_cost = 25; /* TODO: more research on what the total cost is supposed to mean, */ /* relative to the startup cost? */ planstate->total_cost = planstate->startup_cost + baserel->rows; /* Built the (one) path we are providing. Providing fancy paths is */ /* really only possible with back-ends that can properly provide */ /* explain info on how they complete the query, not for something as */ /* obtuse as OGR. (So far, have only seen it w/ the postgres_fdw */ add_path(baserel, (Path*) create_foreignscan_path(root, baserel, #if PG_VERSION_NUM >= 90600 NULL, /* PathTarget */ #endif baserel->rows, planstate->startup_cost, planstate->total_cost, NIL, /* no pathkeys */ NULL, /* no lateral_relids */ NULL /* no extra plan */ #if PG_VERSION_NUM >= 170000 , NIL /* no fdw_restrictinfo list */ #endif #if PG_VERSION_NUM >= 90500 , NIL /* no fdw_private list */ #endif ) ); /* no fdw_private data */ } /* * Convert an OgrFdwSpatialFilter into a List so it can * be safely passed through the fdw_private list. */ static List* ogrSpatialFilterToList(const OgrFdwSpatialFilter* spatial_filter) { List *l = NIL; if (spatial_filter) { l = lappend(l, makeInteger(spatial_filter->ogrfldnum)); l = lappend(l, makeFloat(psprintf("%.17g", spatial_filter->minx))); l = lappend(l, makeFloat(psprintf("%.17g", spatial_filter->miny))); l = lappend(l, makeFloat(psprintf("%.17g", spatial_filter->maxx))); l = lappend(l, makeFloat(psprintf("%.17g", spatial_filter->maxy))); } return l; } /* * Convert the List form back into an OgrFdwSpatialFilter * after passing through fdw_private. */ static OgrFdwSpatialFilter* ogrSpatialFilterFromList(const List* lst) { OgrFdwSpatialFilter* spatial_filter; if (lst == NIL) return NULL; Assert(list_length(lst) == 5); spatial_filter = palloc(sizeof(OgrFdwSpatialFilter)); spatial_filter->ogrfldnum = intVal(linitial(lst)); spatial_filter->minx = floatVal(lsecond(lst)); spatial_filter->miny = floatVal(lthird(lst)); spatial_filter->maxx = floatVal(lfourth(lst)); spatial_filter->maxy = floatVal(list_nth(lst, 4)); /* list_nth counts from zero */ return spatial_filter; } /* * fileGetForeignPlan * Create a ForeignScan plan node for scanning the foreign table */ static ForeignScan* ogrGetForeignPlan(PlannerInfo* root, RelOptInfo* baserel, Oid foreigntableid, ForeignPath* best_path, List* tlist, List* scan_clauses #if PG_VERSION_NUM >= 90500 , Plan* outer_plan #endif ) { Index scan_relid = baserel->relid; bool sql_generated; StringInfoData sql; List* params_list = NULL; List* fdw_private; OgrFdwPlanState* planstate = (OgrFdwPlanState*)(baserel->fdw_private); OgrFdwState* state = (OgrFdwState*)(baserel->fdw_private); OgrFdwSpatialFilter* spatial_filter = NULL; char* attribute_filter = NULL; elog(DEBUG3, "%s: entered function", __func__); /* Add in column mapping data to build SQL with the right OGR column names */ ogrReadColumnData(state); /* * TODO: Review the columns requested (via params_list) and only pull those back, using * OGR_L_SetIgnoredFields. This is less important than pushing restrictions * down to OGR via OGR_L_SetAttributeFilter (done) and (TODO) OGR_L_SetSpatialFilter. */ initStringInfo(&sql); sql_generated = ogrDeparse(&sql, root, baserel, scan_clauses, state, ¶ms_list, &spatial_filter); /* Extract the OGR SQL from the StringInfoData */ if (sql_generated && sql.len > 0) attribute_filter = sql.data; /* Log filters at debug level one as necessary */ if (attribute_filter) elog(DEBUG1, "OGR SQL: %s", attribute_filter); if (spatial_filter) elog(DEBUG1, "OGR spatial filter (%g %g, %g %g)", spatial_filter->minx, spatial_filter->miny, spatial_filter->maxx, spatial_filter->maxy); /* * Here we strip RestrictInfo * nodes from the clauses and ignore pseudoconstants (which will be * handled elsewhere). * Some FDW implementations (mysql_fdw) just pass this full list on to the * make_foreignscan function. postgres_fdw carefully separates local and remote * clauses and only passes the local ones to make_foreignscan, so this * is probably best practice, though re-applying the clauses is probably * the least of our performance worries with this fdw. For now, we just * pass them all to make_foreignscan, see no evil, etc. */ scan_clauses = extract_actual_clauses(scan_clauses, false); /* Pack the data we want to pass to the execution stage into a List. */ /* The members of this list must by copyable by PgSQL, which means */ /* they need to be Lists themselves, or Value nodes, otherwise when */ /* the plan gets copied the copy might fail. */ fdw_private = list_make3(makeString(attribute_filter), params_list, ogrSpatialFilterToList(spatial_filter)); /* Clean up our connection */ ogrFinishConnection(&(planstate->ogr)); /* Create the ForeignScan node */ return make_foreignscan(tlist, scan_clauses, scan_relid, NIL, /* no expressions to evaluate */ fdw_private #if PG_VERSION_NUM >= 90500 , NIL /* no scan_tlist */ , NIL /* no remote quals */ , outer_plan #endif ); } static bool pgCanConvertToOgr(Oid pg_type, OGRFieldType ogr_type) { struct PgOgrMap { Oid pg; OGRFieldType ogr; }; static struct PgOgrMap data[] = { {BOOLOID, OFTInteger}, /* 16 */ {BYTEAOID, OFTBinary}, /* 17 */ {CHAROID, OFTString}, /* 18 */ {NAMEOID, OFTString}, /* 19 */ #if GDAL_VERSION_MAJOR >= 2 {INT8OID, OFTInteger64}, /* 20 */ #else {INT8OID, OFTInteger}, /* 20 */ #endif {INT2OID, OFTInteger}, /* 21 */ {INT4OID, OFTInteger}, /* 23 */ {TEXTOID, OFTString}, /* 25 */ {FLOAT4OID, OFTReal}, /* 700 */ {FLOAT8OID, OFTReal}, /* 701 */ {BOOLARRAYOID, OFTIntegerList}, /* 1000 */ {CHARARRAYOID, OFTStringList}, /* 1002 */ {NAMEARRAYOID, OFTStringList}, /* 1003 */ {INT2ARRAYOID, OFTIntegerList}, /* 1005 */ {INT4ARRAYOID, OFTIntegerList}, /* 1007 */ {TEXTARRAYOID, OFTStringList}, /* 1009 */ {VARCHARARRAYOID, OFTStringList}, /* 1015 */ #if GDAL_VERSION_MAJOR >= 2 {INT8ARRAYOID, OFTInteger64List}, /* 1016 */ #endif {FLOAT4ARRAYOID, OFTRealList}, /* 1021 */ {FLOAT8ARRAYOID, OFTRealList}, /* 1022 */ {BPCHAROID, OFTString}, /* 1042 */ {VARCHAROID, OFTString}, /* 1043 */ {DATEOID, OFTDate}, /* 1082 */ {TIMEOID, OFTTime}, /* 1083 */ {TIMESTAMPOID, OFTDateTime}, /* 1114 */ {NUMERICOID, OFTReal}, /* 1700 */ {0, 0}}; struct PgOgrMap* map = data; while (map->pg) { if (ogr_type == map->ogr) return true; else map++; } return false; } static void pgCheckConvertToOgr(Oid pg_type, OGRFieldType ogr_type, const char *colname, const char *tblname) { if (pgCanConvertToOgr(pg_type, ogr_type)) return; ereport(ERROR, ( errcode(ERRCODE_FDW_INVALID_DATA_TYPE), errmsg("column \"%s\" of foreign table \"%s\" converts \"%s\" to OGR \"%s\"", colname, tblname, format_type_be(pg_type), OGR_GetFieldTypeName(ogr_type)) )); } static bool ogrCanConvertToPg(OGRFieldType ogr_type, Oid pg_type) { struct OgrPgMap { OGRFieldType ogr; Oid pg[16]; }; static struct OgrPgMap data[] = { {OFTInteger, {BOOLOID, INT4OID, INT8OID, NUMERICOID, FLOAT4OID, FLOAT8OID, TEXTOID, VARCHAROID, 0}}, {OFTReal, {NUMERICOID, FLOAT4OID, FLOAT8OID, TEXTOID, VARCHAROID, 0}}, {OFTBinary, {BYTEAOID, 0}}, {OFTString, {TEXTOID, VARCHAROID, CHAROID, BPCHAROID, 0}}, {OFTDate, {DATEOID, TIMESTAMPOID, TEXTOID, VARCHAROID, 0}}, {OFTTime, {TIMEOID, TEXTOID, VARCHAROID, 0}}, {OFTDateTime, {TIMESTAMPOID, TEXTOID, VARCHAROID, 0}}, #if GDAL_VERSION_MAJOR >= 2 {OFTInteger64, {INT8OID, NUMERICOID, FLOAT8OID, TEXTOID, VARCHAROID, 0}}, {OFTInteger64List, {INT8ARRAYOID, FLOAT8ARRAYOID, TEXTARRAYOID, VARCHARARRAYOID, 0}}, #endif {OFTRealList, {FLOAT4ARRAYOID, FLOAT8ARRAYOID, TEXTARRAYOID, VARCHARARRAYOID, 0}}, {OFTStringList, {TEXTARRAYOID, VARCHARARRAYOID, NAMEARRAYOID, CHARARRAYOID, 0}}, {OFTIntegerList, {BOOLARRAYOID, INT2ARRAYOID, INT4ARRAYOID, INT8ARRAYOID, TEXTARRAYOID, VARCHARARRAYOID, 0}}, {256, {0}} /* Zero terminate list */ }; struct OgrPgMap* map = data; while (map->ogr <= OFTMaxType) { if (ogr_type == map->ogr) { Oid *typ = map->pg; while (*typ) { if (pg_type == *typ) return true; else typ++; } } map++; } return false; } static void ogrCheckConvertToPg(OGRFieldType ogr_type, Oid pg_type, const char *colname, const char *tblname) { if (ogrCanConvertToPg(ogr_type, pg_type)) return; if (ogr_type == OFTWideString || ogr_type == OFTWideStringList) { ereport(ERROR, ( errcode(ERRCODE_FDW_INVALID_DATA_TYPE), errmsg("column \"%s\" of foreign table \"%s\" uses an OGR OFTWideString, deprecated", colname, tblname) )); return; } ereport(ERROR, ( errcode(ERRCODE_FDW_INVALID_DATA_TYPE), errmsg("column \"%s\" of foreign table \"%s\" converts OGR \"%s\" to \"%s\"", colname, tblname, OGR_GetFieldTypeName(ogr_type), format_type_be(pg_type)) )); } #ifdef OGR_FDW_HEXWKB static char* hexchr = "0123456789ABCDEF"; static char* ogrBytesToHex(unsigned char* bytes, size_t size) { char* hex; int i; if (! bytes || ! size) { elog(ERROR, "ogrBytesToHex: invalid input"); return NULL; } hex = palloc(size * 2 + 1); hex[2 * size] = '\0'; for (i = 0; i < size; i++) { /* Top four bits to 0-F */ hex[2 * i] = hexchr[bytes[i] >> 4]; /* Bottom four bits to 0-F */ hex[2 * i + 1] = hexchr[bytes[i] & 0x0F]; } return hex; } #endif static void freeOgrFdwTable(OgrFdwTable* table) { if (table) { if (table->tblname) { pfree(table->tblname); } if (table->cols) { pfree(table->cols); } pfree(table); } } typedef struct { char* fldname; int fldnum; } OgrFieldEntry; static int ogrFieldEntryCmpFunc(const void* a, const void* b) { const char* a_name = ((OgrFieldEntry*)a)->fldname; const char* b_name = ((OgrFieldEntry*)b)->fldname; return strcasecmp(a_name, b_name); } /* * The execstate holds a foreign table relation id and an OGR connection, * this function finds all the OGR fields that match up to columns in the * foreign table definition, using columns name match and data type consistency * as the criteria for making a match. * The results of the matching are stored in the execstate before the function * returns. */ static void ogrReadColumnData(OgrFdwState* state) { Relation rel; TupleDesc tupdesc; int i; OgrFdwTable* tbl; OGRFeatureDefnH dfn; int ogr_ncols; int fid_count = 0; int geom_count = 0; int ogr_geom_count = 0; int field_count = 0; OgrFieldEntry* ogr_fields; int ogr_fields_count = 0; char* tblname = get_rel_name(state->foreigntableid); /* Blow away any existing table in the state */ if (state->table) { freeOgrFdwTable(state->table); state->table = NULL; } /* Fresh table */ tbl = palloc0(sizeof(OgrFdwTable)); /* One column for each PgSQL foreign table column */ #if PG_VERSION_NUM < 120000 rel = heap_open(state->foreigntableid, NoLock); #else rel = table_open(state->foreigntableid, NoLock); #endif /* PG_VERSION_NUM */ tupdesc = rel->rd_att; state->tupdesc = tupdesc; tbl->ncols = tupdesc->natts; tbl->cols = palloc0(tbl->ncols * sizeof(OgrFdwColumn)); tbl->tblname = pstrdup(tblname); /* Get OGR metadata ready */ dfn = OGR_L_GetLayerDefn(state->ogr.lyr); ogr_ncols = OGR_FD_GetFieldCount(dfn); #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(1,11,0)) ogr_geom_count = OGR_FD_GetGeomFieldCount(dfn); #else ogr_geom_count = (OGR_FD_GetGeomType(dfn) != wkbNone) ? 1 : 0; #endif /* Prepare sorted list of OGR column names */ /* TODO: change this to a hash table, to avoid repeated strcmp */ /* We will search both the original and laundered OGR field names for matches */ ogr_fields_count = 2 * ogr_ncols; ogr_fields = palloc0(ogr_fields_count * sizeof(OgrFieldEntry)); for (i = 0; i < ogr_ncols; i++) { char* fldname = pstrdup(OGR_Fld_GetNameRef(OGR_FD_GetFieldDefn(dfn, i))); char* fldname_laundered = palloc(STR_MAX_LEN); strncpy(fldname_laundered, fldname, STR_MAX_LEN); ogrStringLaunder(fldname_laundered); ogr_fields[2 * i].fldname = fldname; ogr_fields[2 * i].fldnum = i; ogr_fields[2 * i + 1].fldname = fldname_laundered; ogr_fields[2 * i + 1].fldnum = i; } qsort(ogr_fields, ogr_fields_count, sizeof(OgrFieldEntry), ogrFieldEntryCmpFunc); /* loop through foreign table columns */ for (i = 0; i < tbl->ncols; i++) { List* options; ListCell* lc; OgrFieldEntry* found_entry; OgrFieldEntry entry; #if PG_VERSION_NUM >= 110000 Form_pg_attribute att_tuple = &tupdesc->attrs[i]; #else Form_pg_attribute att_tuple = tupdesc->attrs[i]; #endif OgrFdwColumn col = tbl->cols[i]; col.pgattnum = att_tuple->attnum; col.pgtype = att_tuple->atttypid; col.pgtypmod = att_tuple->atttypmod; col.pgattisdropped = att_tuple->attisdropped; /* Skip filling in any further metadata about dropped columns */ if (col.pgattisdropped) { continue; } /* Check for array type */ col.pgelmtype = get_element_type(col.pgtype); if (col.pgelmtype) { /* Extra type info needed to form the array */ col.pgisarray = true; } else col.pgelmtype = col.pgtype; /* Find the appropriate conversion functions */ getTypeInputInfo(col.pgelmtype, &col.pginputfunc, &col.pginputioparam); getTypeBinaryInputInfo(col.pgelmtype, &col.pgrecvfunc, &col.pgrecvioparam); getTypeOutputInfo(col.pgelmtype, &col.pgoutputfunc, &col.pgoutputvarlena); getTypeBinaryOutputInfo(col.pgelmtype, &col.pgsendfunc, &col.pgsendvarlena); /* Get the PgSQL column name */ #if PG_VERSION_NUM >= 110000 col.pgname = pstrdup(get_attname(rel->rd_id, att_tuple->attnum, false)); #else col.pgname = pstrdup(get_attname(rel->rd_id, att_tuple->attnum)); #endif /* Handle FID first */ if (strcaseeq(col.pgname, "fid") && (col.pgtype == INT4OID || col.pgtype == INT8OID)) { if (fid_count >= 1) { elog(ERROR, "FDW table '%s' includes more than one FID column", tblname); } col.ogrvariant = OGR_FID; col.ogrfldnum = fid_count++; tbl->cols[i] = col; continue; } /* If the OGR source has geometries, can we match them to Pg columns? */ /* We'll match to the first ones we find, irrespective of name */ if (geom_count < ogr_geom_count && col.pgtype == ogrGetGeometryOid()) { col.ogrvariant = OGR_GEOMETRY; col.ogrfldtype = OFTBinary; col.ogrfldnum = geom_count++; tbl->cols[i] = col; continue; } /* Now we search for matches in the OGR fields */ /* By default, search for the PgSQL column name */ entry.fldname = col.pgname; entry.fldnum = 0; /* * But, if there is a 'column_name' option for this column, we * want to search for *that* in the OGR layer. */ options = GetForeignColumnOptions(state->foreigntableid, i + 1); foreach (lc, options) { DefElem* def = (DefElem*) lfirst(lc); if (streq(def->defname, OPT_COLUMN)) { entry.fldname = defGetString(def); break; } } /* Search PgSQL column name in the OGR column name list */ found_entry = bsearch(&entry, ogr_fields, ogr_fields_count, sizeof(OgrFieldEntry), ogrFieldEntryCmpFunc); /* Column name matched, so save this entry, if the types are consistent */ if (found_entry) { OGRFieldDefnH fld = OGR_FD_GetFieldDefn(dfn, found_entry->fldnum); OGRFieldType fldtype = OGR_Fld_GetType(fld); /* Error if types mismatched when column names match */ ogrCheckConvertToPg(fldtype, col.pgtype, col.pgname, tblname); col.ogrvariant = OGR_FIELD; col.ogrfldnum = found_entry->fldnum; col.ogrfldtype = fldtype; field_count++; } else { col.ogrvariant = OGR_UNMATCHED; } tbl->cols[i] = col; } elog(DEBUG2, "ogrReadColumnData matched %d FID, %d GEOM, %d FIELDS out of %d PGSQL COLUMNS", fid_count, geom_count, field_count, tbl->ncols); /* Clean up */ state->table = tbl; for (i = 0; i < 2 * ogr_ncols; i++) { if (ogr_fields[i].fldname) { pfree(ogr_fields[i].fldname); } } pfree(ogr_fields); #if PG_VERSION_NUM < 120000 heap_close(rel, NoLock); #else table_close(rel, NoLock); #endif /* PG_VERSION_NUM */ return; } /* * ogrLookupGeometryFunctionOid * * Find the procedure Oids of useful functions so we can call * them later. */ static Oid ogrLookupGeometryFunctionOid(const char* proname) { List* names; FuncCandidateList clist; /* This only works if PostGIS is installed */ if (ogrGetGeometryOid() == InvalidOid || ogrGetGeometryOid() == BYTEAOID) { return InvalidOid; } #if PG_VERSION_NUM < 160000 names = stringToQualifiedNameList(proname); #else names = stringToQualifiedNameList(proname,NULL); #endif #if PG_VERSION_NUM < 90400 clist = FuncnameGetCandidates(names, -1, NIL, false, false); #elif PG_VERSION_NUM < 140000 clist = FuncnameGetCandidates(names, -1, NIL, false, false, false); #else clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, false); #endif if (streq(proname, "st_setsrid")) { do { int i; for (i = 0; i < clist->nargs; i++) { if (clist->args[i] == ogrGetGeometryOid()) { return clist->oid; } } } while ((clist = clist->next)); } else if (streq(proname, "postgis_typmod_srid")) { return clist->oid; } return InvalidOid; } /* * ogrBeginForeignScan */ static void ogrBeginForeignScan(ForeignScanState* node, int eflags) { OgrFdwState* state; OgrFdwExecState* execstate; OgrFdwSpatialFilter* spatial_filter; Oid foreigntableid = RelationGetRelid(node->ss.ss_currentRelation); ForeignScan* fsplan = (ForeignScan*)node->ss.ps.plan; elog(DEBUG3, "%s: entered function", __func__); /* Do nothing in EXPLAIN (no ANALYZE) case */ if (eflags & EXEC_FLAG_EXPLAIN_ONLY) return; /* Initialize OGR connection */ state = getOgrFdwState(foreigntableid, OGR_EXEC_STATE); execstate = (OgrFdwExecState*)state; /* Read the OGR layer definition and PgSQL foreign table definitions */ ogrReadColumnData(state); /* Collect the procedure Oids for PostGIS functions we might need */ execstate->setsridfunc = ogrLookupGeometryFunctionOid("st_setsrid"); execstate->typmodsridfunc = ogrLookupGeometryFunctionOid("postgis_typmod_srid"); /* Get OGR SQL generated by the deparse step during the planner function. */ execstate->sql = (char*) strVal(list_nth(fsplan->fdw_private, 0)); /* TODO: Use the parse step attribute list to restrict requested columns */ // execstate->retrieved_attrs = (List *) list_nth(fsplan->fdw_private, 1); /* Get spatial filter generated by the deparse step. */ spatial_filter = ogrSpatialFilterFromList(list_nth(fsplan->fdw_private, 2)); if (spatial_filter) { OGR_L_SetSpatialFilterRectEx(execstate->ogr.lyr, spatial_filter->ogrfldnum, spatial_filter->minx, spatial_filter->miny, spatial_filter->maxx, spatial_filter->maxy ); } if (execstate->sql && strlen(execstate->sql) > 0) { OGRErr err = OGR_L_SetAttributeFilter(execstate->ogr.lyr, execstate->sql); if (err != OGRERR_NONE) { const char* ogrerr = CPLGetLastErrorMsg(); if (ogrerr && ! streq(ogrerr, "")) { ereport(NOTICE, (errcode(ERRCODE_FDW_ERROR), errmsg("unable to set OGR SQL '%s' on layer", execstate->sql), errhint("%s", ogrerr))); } else { ereport(NOTICE, (errcode(ERRCODE_FDW_ERROR), errmsg("unable to set OGR SQL '%s' on layer", execstate->sql))); } } } else { OGR_L_SetAttributeFilter(execstate->ogr.lyr, NULL); } /* Save the state for the next call */ node->fdw_state = (void*) execstate; return; } /* * Rather than explicitly try and form PgSQL datums, use the type * input functions, that accept cstring representations, and convert * to the input format. We have to lookup the right input function for * each column in the foreign table. */ static Datum pgDatumFromCString(const char* cstr, const OgrFdwColumn *col, int char_encoding, bool *is_null) { size_t cstr_len = cstr ? strlen(cstr) : 0; Datum value = (Datum) 0; char *cstr_decoded; /* Zero length implies NULL for all non-strings */ if (cstr_len == 0 && (col->ogrfldtype != OFTString && col->ogrfldtype != OFTStringList)) { *is_null = true; return value; } cstr_decoded = char_encoding ? pg_any_to_server(cstr, cstr_len, char_encoding) : pstrdup(cstr); value = OidFunctionCall3(col->pginputfunc, CStringGetDatum(cstr_decoded), ObjectIdGetDatum(InvalidOid), Int32GetDatum(col->pgtypmod)); /* Free cstr_decoded if it is a copy */ if (cstr != cstr_decoded) pfree(cstr_decoded); is_null = false; return value; } static inline void ogrNullSlot(Datum* values, bool* nulls, int i) { values[i] = PointerGetDatum(NULL); nulls[i] = true; } /* * The ogrIterateForeignScan is getting a new TupleTableSlot to handle * for each iteration. Each slot contains an entry for every column in * in the foreign table, that has to be filled out, either with a value * or a NULL for columns that either have been deleted or were not requested * in the query. * * The tupledescriptor tells us about the types of each slot. * For now we assume our slot has exactly the same number of * records and equivalent types to our OGR layer, and that our * foreign table's first two columns are an integer primary key * using int8 as the type, and then a geometry using bytea as * the type, then everything else. */ static OGRErr ogrFeatureToSlot(const OGRFeatureH feat, TupleTableSlot* slot, const OgrFdwExecState* execstate) { const OgrFdwTable* tbl = execstate->table; int i; Datum* values = slot->tts_values; bool* nulls = slot->tts_isnull; TupleDesc tupdesc = slot->tts_tupleDescriptor; int have_typmod_funcs = (execstate->setsridfunc && execstate->typmodsridfunc); #define CSTR_SZ 256 char cstr[CSTR_SZ]; /* Check our assumption that slot and setup data match */ if (tbl->ncols != tupdesc->natts) { elog(ERROR, "FDW metadata table and exec table have mismatching number of columns"); return OGRERR_FAILURE; } /* For each pgtable column, get a value from OGR */ for (i = 0; i < tbl->ncols; i++) { OgrFdwColumn col = tbl->cols[i]; const char* pgname = col.pgname; Oid pgtype = col.pgtype; int ogrfldnum = col.ogrfldnum; OGRFieldType ogrfldtype = col.ogrfldtype; OgrColumnVariant ogrvariant = col.ogrvariant; /* * Fill in dropped attributes with NULL */ if (col.pgattisdropped) { ogrNullSlot(values, nulls, i); continue; } if (ogrvariant == OGR_FID) { GIntBig fid = OGR_F_GetFID(feat); bool is_null = false; if (fid == OGRNullFID) { ogrNullSlot(values, nulls, i); } else { char fidstr[256]; snprintf(fidstr, 256, OGR_FDW_FRMT_INT64, OGR_FDW_CAST_INT64(fid)); values[i] = pgDatumFromCString(fidstr, &col, execstate->ogr.char_encoding, &is_null); nulls[i] = is_null; } } else if (ogrvariant == OGR_GEOMETRY) { int wkbsize; int varsize; bytea* varlena; unsigned char* wkb; OGRErr err; #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(1,11,0)) OGRGeometryH geom = OGR_F_GetGeomFieldRef(feat, ogrfldnum); #else OGRGeometryH geom = OGR_F_GetGeometryRef(feat); #endif /* No geometry ? NULL */ if (! geom) { /* No geometry column, so make the output null */ ogrNullSlot(values, nulls, i); continue; } /* * Start by generating standard PgSQL variable length byte * buffer, with WKB filled into the data area. */ wkbsize = OGR_G_WkbSize(geom); varsize = wkbsize + VARHDRSZ; varlena = palloc(varsize); wkb = (unsigned char*)VARDATA(varlena); err = OGR_G_ExportToWkb(geom, wkbNDR, wkb); SET_VARSIZE(varlena, varsize); /* Couldn't create WKB from OGR geometry? error */ if (err != OGRERR_NONE) { return err; } if (pgtype == BYTEAOID) { /* * Nothing special to do for bytea, just send the varlena data through! */ nulls[i] = false; values[i] = PointerGetDatum(varlena); } else if (pgtype == ogrGetGeometryOid()) { /* * For geometry we need to convert the varlena WKB data into a serialized * geometry (aka "gserialized"). For that, we can use the type's "recv" function * which takes in WKB and spits out serialized form, or the "input" function * that takes in HEXWKB. The "input" function is more lax about geometry * structure errors (unclosed polys, etc). */ #ifdef OGR_FDW_HEXWKB char* hexwkb = ogrBytesToHex(wkb, wkbsize); /* * Use the input function to convert the WKB from OGR into * a PostGIS internal format. */ nulls[i] = false; values[i] = OidFunctionCall1(col.pginputfunc, PointerGetDatum(hexwkb)); pfree(hexwkb); #else /* * The "recv" function expects to receive a StringInfo pointer * on the first argument, so we form one of those ourselves by * hand. Rather than copy into a fresh buffer, we'll just use the * existing varlena buffer and point to the data area. * * The "recv" function tests for basic geometry validity, * things like polygon closure, etc. So don't feed it junk. */ StringInfoData strinfo; strinfo.data = (char*)wkb; strinfo.len = wkbsize; strinfo.maxlen = strinfo.len; strinfo.cursor = 0; /* * Use the recv function to convert the WKB from OGR into * a PostGIS internal format. */ nulls[i] = false; values[i] = OidFunctionCall1(col.pgrecvfunc, PointerGetDatum(&strinfo)); #endif /* * Apply the typmod restriction to the incoming geometry, so it's * not really a restriction anymore, it's more like a requirement. * * TODO: In the case where the OGR input actually *knows* what SRID * it is, we should actually apply *that* and let the restriction run * its usual course. */ if (have_typmod_funcs && col.pgtypmod >= 0) { Datum srid = OidFunctionCall1(execstate->typmodsridfunc, Int32GetDatum(col.pgtypmod)); values[i] = OidFunctionCall2(execstate->setsridfunc, values[i], srid); } } else { elog(NOTICE, "conversion to geometry called with column type not equal to bytea or geometry"); ogrNullSlot(values, nulls, i); } } else if (ogrvariant == OGR_FIELD) { #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,2,0)) int field_not_null = OGR_F_IsFieldSet(feat, ogrfldnum) && ! OGR_F_IsFieldNull(feat, ogrfldnum); #else int field_not_null = OGR_F_IsFieldSet(feat, ogrfldnum); #endif /* Ensure that the OGR data type fits the destination Pg column */ ogrCheckConvertToPg(ogrfldtype, pgtype, pgname, tbl->tblname); /* Only convert non-null fields */ if (!field_not_null) { ogrNullSlot(values, nulls, i); continue; } switch (ogrfldtype) { case OFTBinary: { /* * Convert binary fields to bytea directly */ int bufsize; GByte* buf = OGR_F_GetFieldAsBinary(feat, ogrfldnum, &bufsize); int varsize = bufsize + VARHDRSZ; bytea* varlena = palloc(varsize); memcpy(VARDATA(varlena), buf, bufsize); SET_VARSIZE(varlena, varsize); nulls[i] = false; values[i] = PointerGetDatum(varlena); break; } case OFTInteger: case OFTReal: case OFTString: #if GDAL_VERSION_MAJOR >= 2 case OFTInteger64: #endif { /* * Convert numbers and strings via a string representation. * Handling numbers directly would be faster, but require a lot of extra code. * For now, we go via text. */ const char* cstr_in = OGR_F_GetFieldAsString(feat, ogrfldnum); bool is_null = false; values[i] = pgDatumFromCString(cstr_in, &col, execstate->ogr.char_encoding, &is_null); nulls[i] = is_null; break; } case OFTDate: case OFTTime: case OFTDateTime: { /* * OGR date/times have a weird access method, so we use that to pull * out the raw data and turn it into a string for PgSQL's (very * sophisticated) date/time parsing routines to handle. */ int year, month, day, hour, minute, second, tz; bool is_null = false; OGR_F_GetFieldAsDateTime(feat, ogrfldnum, &year, &month, &day, &hour, &minute, &second, &tz); if (ogrfldtype == OFTDate) { snprintf(cstr, CSTR_SZ, "%d-%02d-%02d", year, month, day); } else if (ogrfldtype == OFTTime) { snprintf(cstr, CSTR_SZ, "%02d:%02d:%02d", hour, minute, second); } else { #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)) const char* tsstr = OGR_F_GetFieldAsISO8601DateTime(feat, ogrfldnum, NULL); strncpy(cstr, tsstr, CSTR_SZ); #else snprintf(cstr, CSTR_SZ, "%d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second); #endif } values[i] = pgDatumFromCString(cstr, &col, PG_SQL_ASCII, &is_null); nulls[i] = is_null; break; } #if GDAL_VERSION_MAJOR >= 2 case OFTInteger64List: { int ilist_size; const int64 *ilist = (int64*)OGR_F_GetFieldAsInteger64List(feat, ogrfldnum, &ilist_size); ArrayBuildState *abs = initArrayResult(col.pgelmtype, CurrentMemoryContext, false); for (uint32 i = 0; i < ilist_size; i++) { bool is_null = false; snprintf(cstr, CSTR_SZ, "%ld", ilist[i]); abs = accumArrayResult(abs, pgDatumFromCString(cstr, &col, execstate->ogr.char_encoding, &is_null), is_null, col.pgelmtype, CurrentMemoryContext); } values[i] = makeArrayResult(abs, CurrentMemoryContext); nulls[i] = false; break; } #endif case OFTIntegerList: { int ilist_size; const int *ilist = OGR_F_GetFieldAsIntegerList(feat, ogrfldnum, &ilist_size); ArrayBuildState *abs = initArrayResult(col.pgelmtype, CurrentMemoryContext, false); for (uint32 i = 0; i < ilist_size; i++) { bool is_null = false; snprintf(cstr, CSTR_SZ, "%d", ilist[i]); abs = accumArrayResult(abs, pgDatumFromCString(cstr, &col, execstate->ogr.char_encoding, &is_null), is_null, col.pgelmtype, CurrentMemoryContext); } values[i] = makeArrayResult(abs, CurrentMemoryContext); nulls[i] = false; break; } case OFTRealList: { int rlist_size; const double *rlist = OGR_F_GetFieldAsDoubleList(feat, ogrfldnum, &rlist_size); ArrayBuildState *abs = initArrayResult(col.pgelmtype, CurrentMemoryContext, false); for (uint32 i = 0; i < rlist_size; i++) { bool is_null = false; snprintf(cstr, CSTR_SZ, "%g", rlist[i]); abs = accumArrayResult(abs, pgDatumFromCString(cstr, &col, execstate->ogr.char_encoding, &is_null), is_null, col.pgelmtype, CurrentMemoryContext); } values[i] = makeArrayResult(abs, CurrentMemoryContext); nulls[i] = false; break; } case OFTStringList: { ArrayBuildState *abs = initArrayResult(col.pgelmtype, CurrentMemoryContext, false); char **cstrs = OGR_F_GetFieldAsStringList(feat, ogrfldnum); while (*cstrs) { bool is_null = false; abs = accumArrayResult(abs, pgDatumFromCString(*cstrs, &col, execstate->ogr.char_encoding, &is_null), is_null, col.pgelmtype, CurrentMemoryContext); cstrs++; } values[i] = makeArrayResult(abs, CurrentMemoryContext); nulls[i] = false; break; } default: { elog(ERROR, "unsupported OGR type \"%s\"", OGR_GetFieldTypeName(ogrfldtype)); break; } } /* !switch */ } /* Fill in unmatched columns with NULL */ else if (ogrvariant == OGR_UNMATCHED) { ogrNullSlot(values, nulls, i); } else { elog(ERROR, "OGR FDW unsupported column variant in \"%s\", %d", pgname, ogrvariant); return OGRERR_FAILURE; } } /* done! */ return OGRERR_NONE; } static void ogrStaticText(char* text, const char* str) { size_t len = strlen(str); memcpy(VARDATA(text), str, len); SET_VARSIZE(text, len + VARHDRSZ); return; } /* * EWKB includes a flag that indicates an SRID embedded in the * binary. The EWKB has an endian byte, four bytes of type information * and then 4 bytes of optional SRID information. If that info is * there, we want to over-write it, and remove the SRID flag, to * generate more "standard" WKB for OGR to consume. */ static size_t ogrEwkbStripSrid(unsigned char* wkb, size_t wkbsize) { unsigned int type = 0; int has_srid = 0; size_t newwkbsize = wkbsize; memcpy(&type, wkb + 1, 4); /* has_z = type & 0x80000000; */ /* has_m = type & 0x40000000; */ has_srid = type & 0x20000000; /* Flatten SRID flag away */ type &= 0xDFFFFFFF; memcpy(wkb + 1, &type, 4); /* If there was an SRID number embedded, overwrite it */ if (has_srid) { newwkbsize -= 4; /* no space for SRID number needed */ memmove(wkb + 5, wkb + 9, newwkbsize - 5); } return newwkbsize; } OGRErr pgDatumToOgrGeometry (Datum pg_geometry, Oid pgsendfunc, OGRGeometryH* ogr_geometry) { OGRErr err; bytea* wkb_bytea = DatumGetByteaP(OidFunctionCall1(pgsendfunc, pg_geometry)); unsigned char* wkb = (unsigned char*)VARDATA_ANY(wkb_bytea); size_t wkbsize = VARSIZE_ANY_EXHDR(wkb_bytea); wkbsize = ogrEwkbStripSrid(wkb, wkbsize); err = OGR_G_CreateFromWkb(wkb, NULL, ogr_geometry, wkbsize); if (wkb_bytea) pfree(wkb_bytea); return err; } static OGRErr ogrSlotToFeature(const TupleTableSlot* slot, OGRFeatureH feat, const OgrFdwTable* tbl) { int i; Datum* values = slot->tts_values; bool* nulls = slot->tts_isnull; TupleDesc tupdesc = slot->tts_tupleDescriptor; int year, month, day, hour, minute, second; /* Prepare date-time part tokens for use later */ char txtyear[STR_MAX_LEN]; char txtmonth[STR_MAX_LEN]; char txtday[STR_MAX_LEN]; char txthour[STR_MAX_LEN]; char txtminute[STR_MAX_LEN]; char txtsecond[STR_MAX_LEN]; ogrStaticText(txtyear, "year"); ogrStaticText(txtmonth, "month"); ogrStaticText(txtday, "day"); ogrStaticText(txthour, "hour"); ogrStaticText(txtminute, "minute"); ogrStaticText(txtsecond, "second"); /* Check our assumption that slot and setup data match */ if (tbl->ncols != tupdesc->natts) { elog(ERROR, "FDW metadata table and slot table have mismatching number of columns"); return OGRERR_FAILURE; } /* For each pgtable column, set a value on the feature OGR */ for (i = 0; i < tbl->ncols; i++) { OgrFdwColumn col = tbl->cols[i]; const char* pgname = col.pgname; Oid pgtype = col.pgtype; Oid pgoutputfunc = col.pgoutputfunc; int ogrfldnum = col.ogrfldnum; OGRFieldType ogrfldtype = col.ogrfldtype; OgrColumnVariant ogrvariant = col.ogrvariant; /* Skip dropped attributes */ if (col.pgattisdropped) { continue; } /* Skip the FID, we have to treat it as immutable anyways */ if (ogrvariant == OGR_FID) { if (nulls[i]) { OGR_F_SetFID(feat, OGRNullFID); } else { if (pgtype == INT4OID) { int32 val = DatumGetInt32(values[i]); OGR_F_SetFID(feat, val); } else if (pgtype == INT8OID) { int64 val = DatumGetInt64(values[i]); OGR_F_SetFID(feat, val); } else { elog(ERROR, "unable to handle non-integer fid"); } } continue; } /* TODO: For updates, we should only set the fields that are */ /* in the target list, and flag the others as unchanged */ if (ogrvariant == OGR_GEOMETRY) { OGRErr err; if (nulls[i]) { #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(1,11,0)) err = OGR_F_SetGeomFieldDirectly(feat, ogrfldnum, NULL); #else err = OGR_F_SetGeometryDirectly(feat, NULL); #endif continue; } else { OGRGeometryH geom; err = pgDatumToOgrGeometry (values[i], col.pgsendfunc, &geom); if (err != OGRERR_NONE) return err; #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(1,11,0)) err = OGR_F_SetGeomFieldDirectly(feat, ogrfldnum, geom); #else err = OGR_F_SetGeometryDirectly(feat, geom); #endif } } else if (ogrvariant == OGR_FIELD) { /* Ensure that the OGR data type fits the destination Pg column */ pgCheckConvertToOgr(pgtype, ogrfldtype, pgname, tbl->tblname); /* Skip NULL case */ if (nulls[i]) { OGR_F_UnsetField(feat, ogrfldnum); continue; } switch (pgtype) { case BOOLOID: { int8 val = DatumGetBool(values[i]); OGR_F_SetFieldInteger(feat, ogrfldnum, val); break; } case INT2OID: { int16 val = DatumGetInt16(values[i]); OGR_F_SetFieldInteger(feat, ogrfldnum, val); break; } case INT4OID: { int32 val = DatumGetInt32(values[i]); OGR_F_SetFieldInteger(feat, ogrfldnum, val); break; } case INT8OID: { int64 val = DatumGetInt64(values[i]); #if GDAL_VERSION_MAJOR >= 2 OGR_F_SetFieldInteger64(feat, ogrfldnum, val); #else if (val < INT_MAX) { OGR_F_SetFieldInteger(feat, ogrfldnum, (int32)val); } else { elog(ERROR, "unable to coerce int64 into int32 OGR field"); } #endif break; } case NUMERICOID: { Datum d; float8 f; /* Convert to string */ d = OidFunctionCall1(pgoutputfunc, values[i]); /* Convert back to float8 */ f = DatumGetFloat8(DirectFunctionCall1(float8in, d)); OGR_F_SetFieldDouble(feat, ogrfldnum, f); break; } case FLOAT4OID: { OGR_F_SetFieldDouble(feat, ogrfldnum, DatumGetFloat4(values[i])); break; } case FLOAT8OID: { OGR_F_SetFieldDouble(feat, ogrfldnum, DatumGetFloat8(values[i])); break; } case TEXTOID: case VARCHAROID: case NAMEOID: case BPCHAROID: /* char(n) */ { bytea* varlena = (bytea*)DatumGetPointer(values[i]); size_t varsize = VARSIZE_ANY_EXHDR(varlena); char* str = palloc0(varsize + 1); memcpy(str, VARDATA_ANY(varlena), varsize); OGR_F_SetFieldString(feat, ogrfldnum, str); pfree(str); break; } case CHAROID: /* char */ { char str[2]; str[0] = DatumGetChar(values[i]); str[1] = '\0'; OGR_F_SetFieldString(feat, ogrfldnum, str); break; } case BYTEAOID: { bytea* varlena = PG_DETOAST_DATUM(values[i]); size_t varsize = VARSIZE_ANY_EXHDR(varlena); OGR_F_SetFieldBinary(feat, ogrfldnum, varsize, (GByte*)VARDATA_ANY(varlena)); break; } case DATEOID: { /* Convert date to timestamp */ Datum d = DirectFunctionCall1(date_timestamp, values[i]); /* Read out the parts */ year = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtyear), d))); month = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtmonth), d))); day = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtday), d))); OGR_F_SetFieldDateTime(feat, ogrfldnum, year, month, day, 0, 0, 0, 0); break; } /* TODO: handle time zones explicitly */ case TIMEOID: case TIMETZOID: { /* Read the parts of the time */ hour = lround(DatumGetFloat8(DirectFunctionCall2(time_part, PointerGetDatum(txthour), values[i]))); minute = lround(DatumGetFloat8(DirectFunctionCall2(time_part, PointerGetDatum(txtminute), values[i]))); second = lround(DatumGetFloat8(DirectFunctionCall2(time_part, PointerGetDatum(txtsecond), values[i]))); OGR_F_SetFieldDateTime(feat, ogrfldnum, 0, 0, 0, hour, minute, second, 0); break; } case TIMESTAMPOID: case TIMESTAMPTZOID: { Datum d = values[i]; year = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtyear), d))); month = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtmonth), d))); day = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtday), d))); hour = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txthour), d))); minute = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtminute), d))); second = lround(DatumGetFloat8(DirectFunctionCall2(timestamp_part, PointerGetDatum(txtsecond), d))); OGR_F_SetFieldDateTime(feat, ogrfldnum, year, month, day, hour, minute, second, 0); break; } case BOOLARRAYOID: case INT2ARRAYOID: case INT4ARRAYOID: { ArrayType *arr = DatumGetArrayTypeP(values[i]); size_t sz = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); Datum d; bool isnull; int *ints = palloc(sizeof(int) * sz); int num_ints = 0; ArrayIterator it = array_create_iterator(arr, 0, NULL); while (array_iterate(it, &d, &isnull)) { if (isnull) continue; ints[num_ints++] = DatumGetInt32(d); } OGR_F_SetFieldIntegerList(feat, ogrfldnum, num_ints, ints); pfree(ints); break; } case CHARARRAYOID: case NAMEARRAYOID: case TEXTARRAYOID: case VARCHARARRAYOID: { ArrayType *arr = DatumGetArrayTypeP(values[i]); Datum d; bool isnull; char** papszList = NULL; ArrayIterator it = array_create_iterator(arr, 0, NULL); while (array_iterate(it, &d, &isnull)) { char *cstr; if (isnull) continue; cstr = text_to_cstring(DatumGetTextP(d)); papszList = CSLAddString(papszList, cstr); pfree(cstr); } OGR_F_SetFieldStringList(feat, ogrfldnum, papszList); CSLDestroy(papszList); break; } case FLOAT4ARRAYOID: case FLOAT8ARRAYOID: { ArrayType *arr = DatumGetArrayTypeP(values[i]); size_t sz = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); Datum d; bool isnull; double *floats = palloc(sizeof(double) * sz); int num_floats = 0; ArrayIterator it = array_create_iterator(arr, 0, NULL); while (array_iterate(it, &d, &isnull)) { if (isnull) continue; floats[num_floats++] = DatumGetFloat8(d); } OGR_F_SetFieldDoubleList(feat, ogrfldnum, num_floats, floats); pfree(floats); break; } /* TODO: array types for string, integer, float */ default: { elog(ERROR, "OGR FDW unsupported PgSQL column type in \"%s\", %d", pgname, pgtype); return OGRERR_FAILURE; } } } /* Fill in unmatched columns with NULL */ else if (ogrvariant == OGR_UNMATCHED) { OGR_F_UnsetField(feat, ogrfldnum); } else { elog(ERROR, "OGR FDW unsupported column variant in \"%s\", %d", pgname, ogrvariant); return OGRERR_FAILURE; } } /* done! */ return OGRERR_NONE; } /* * ogrIterateForeignScan * Read next record from OGR and store it into the * ScanTupleSlot as a virtual tuple */ static TupleTableSlot* ogrIterateForeignScan(ForeignScanState* node) { OgrFdwExecState* execstate = (OgrFdwExecState*) node->fdw_state; TupleTableSlot* slot = node->ss.ss_ScanTupleSlot; OGRFeatureH feat; elog(DEBUG3, "%s: entered function", __func__); /* * Clear the slot. If it gets through w/o being filled up, that means * we're all done. */ ExecClearTuple(slot); /* * First time through, reset reading. Then keep reading until * we run out of records, then return a cleared (NULL) slot, to * notify the core we're done. */ if (execstate->rownum == 0) { OGR_L_ResetReading(execstate->ogr.lyr); } /* If we rectreive a feature from OGR, copy it over into the slot */ feat = OGR_L_GetNextFeature(execstate->ogr.lyr); if (feat) { /* convert result to arrays of values and null indicators */ if (OGRERR_NONE != ogrFeatureToSlot(feat, slot, execstate)) { ogrEreportError("failure reading OGR data source"); } /* store the virtual tuple */ ExecStoreVirtualTuple(slot); /* increment row count */ execstate->rownum++; /* Release OGR feature object */ OGR_F_Destroy(feat); } return slot; } /* * ogrReScanForeignScan * Rescan table, possibly with new parameters */ static void ogrReScanForeignScan(ForeignScanState* node) { OgrFdwExecState* execstate = (OgrFdwExecState*) node->fdw_state; elog(DEBUG3, "%s: entered function", __func__); OGR_L_ResetReading(execstate->ogr.lyr); execstate->rownum = 0; return; } /* * ogrEndForeignScan * Finish scanning foreign table and dispose objects used for this scan */ static void ogrEndForeignScan(ForeignScanState* node) { OgrFdwExecState* execstate = (OgrFdwExecState*) node->fdw_state; elog(DEBUG3, "%s: entered function", __func__); if (execstate) { elog(DEBUG2, "OGR FDW processed %d rows from OGR", execstate->rownum); ogrFinishConnection(&(execstate->ogr)); } return; } /* ======================================================== */ /* WRITE SUPPORT */ /* ======================================================== */ /* if the scanning functions above respected the targetlist, we would only be getting back the SET target=foo columns in the slots below, so we would need to add the "fid" to all targetlists (and also disallow fid changing perhaps). since we always pull complete tables in the scan functions, the slots below are basically full tables, in fact they include (?) one entry for each OGR column, even when the table does not include the column, just nulling out the entries that are not in the table definition it might be better to update the scan code to properly manage target lists first, and then come back here and do things properly we will need a ogrSlotToFeature to feed into the OGR_L_SetFeature and OGR_L_CreateFeature functions. Also will use OGR_L_DeleteFeature and fid value in ogrGetForeignPlan we get a tlist that includes just the attributes we are interested in, can use that to pare down the request perhaps */ static int ogrGetFidColumn(const TupleDesc td) { int i; for (i = 0; i < td->natts; i++) { #if PG_VERSION_NUM >= 110000 NameData attname = td->attrs[i].attname; Oid atttypeid = td->attrs[i].atttypid; #else NameData attname = td->attrs[i]->attname; Oid atttypeid = td->attrs[i]->atttypid; #endif if ((atttypeid == INT4OID || atttypeid == INT8OID) && strcaseeq("fid", attname.data)) { return i; } } return -1; } /* * ogrAddForeignUpdateTargets * * For now we no-op this callback, as we are making the presence of * "fid" in the FDW table definition a requirement for any update. * It might be possible to add nonexisting "junk" columns? In which case * there could always be a virtual fid travelling with the queries, * and the FDW table itself wouldn't need such a column? */ #if PG_VERSION_NUM >= 140000 static void ogrAddForeignUpdateTargets(PlannerInfo* planinfo, unsigned int rte_index, RangeTblEntry* target_rte, Relation target_relation) { Query* parsetree = planinfo->parse; Form_pg_attribute att; Var* var; TupleDesc tupdesc = target_relation->rd_att; int fid_column = ogrGetFidColumn(tupdesc); elog(DEBUG3, "%s: entered function", __func__); if (fid_column < 0) { elog(ERROR, "table '%s' does not have a 'fid' column", RelationGetRelationName(target_relation)); } att = &tupdesc->attrs[fid_column]; /* Make a Var representing the desired value */ var = makeVar(parsetree->resultRelation, att->attnum, att->atttypid, att->atttypmod, att->attcollation, 0); add_row_identity_var(planinfo, var, rte_index, "fid"); } #else /* PG_VERSION_NUM < 140000 */ static void ogrAddForeignUpdateTargets(Query* parsetree, RangeTblEntry* target_rte, Relation target_relation) { ListCell* cell; Form_pg_attribute att; Var* var; TargetEntry* tle; TupleDesc tupdesc = target_relation->rd_att; int fid_column = ogrGetFidColumn(tupdesc); elog(DEBUG3, "%s: entered function", __func__); if (fid_column < 0) { elog(ERROR, "table '%s' does not have a 'fid' column", RelationGetRelationName(target_relation)); } #if PG_VERSION_NUM >= 110000 att = &tupdesc->attrs[fid_column]; #else att = tupdesc->attrs[fid_column]; #endif /* Make a Var representing the desired value */ var = makeVar(parsetree->resultRelation, att->attnum, att->atttypid, att->atttypmod, att->attcollation, 0); /* Wrap it in a resjunk TLE with the right name ... */ tle = makeTargetEntry((Expr*)var, list_length(parsetree->targetList) + 1, pstrdup(NameStr(att->attname)), true); parsetree->targetList = lappend(parsetree->targetList, tle); foreach (cell, parsetree->targetList) { TargetEntry* target = (TargetEntry*) lfirst(cell); elog(DEBUG4, "parsetree->targetList %s:%d", target->resname, target->resno); } } #endif /* * ogrBeginForeignModify * For now the only thing we'll do here is set up the connection * and pass that on to the next functions. */ static void ogrBeginForeignModify(ModifyTableState* mtstate, ResultRelInfo* rinfo, List* fdw_private, int subplan_index, int eflags) { Oid foreigntableid; OgrFdwState* state; elog(DEBUG3, "%s: entered function", __func__); foreigntableid = RelationGetRelid(rinfo->ri_RelationDesc); state = getOgrFdwState(foreigntableid, OGR_MODIFY_STATE); /* Read the OGR layer definition and PgSQL foreign table definitions */ ogrReadColumnData(state); /* Save OGR connection, etc, for later */ rinfo->ri_FdwState = state; return; } /* * ogrExecForeignUpdate * Find out what the fid is, get the OGR feature for that FID, * and then update the values on that feature. */ static TupleTableSlot* ogrExecForeignUpdate(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot) { OgrFdwModifyState* modstate = rinfo->ri_FdwState; TupleDesc td = slot->tts_tupleDescriptor; Relation rel = rinfo->ri_RelationDesc; Oid foreigntableid = RelationGetRelid(rel); int fid_column; Oid fid_type; Datum fid_datum; int64 fid; OGRFeatureH feat; OGRErr err; elog(DEBUG3, "%s: entered function", __func__); /* Is there a fid column? */ fid_column = ogrGetFidColumn(td); if (fid_column < 0) { elog(ERROR, "cannot find 'fid' column in table '%s'", get_rel_name(foreigntableid)); } #if PG_VERSION_NUM >= 120000 slot_getallattrs(slot); #endif /* What is the value of the FID for this record? */ fid_datum = slot->tts_values[fid_column]; #if PG_VERSION_NUM >= 110000 fid_type = td->attrs[fid_column].atttypid; #else fid_type = td->attrs[fid_column]->atttypid; #endif if (fid_type == INT8OID) { fid = DatumGetInt64(fid_datum); } else { fid = DatumGetInt32(fid_datum); } elog(DEBUG2, "ogrExecForeignUpdate fid=" OGR_FDW_FRMT_INT64, OGR_FDW_CAST_INT64(fid)); /* Get the OGR feature for this fid */ feat = OGR_L_GetFeature(modstate->ogr.lyr, fid); /* If we found a feature, then copy data from the slot onto the feature */ /* and then back into the layer */ if (! feat) { ogrEreportError("failure reading OGR feature"); } err = ogrSlotToFeature(slot, feat, modstate->table); if (err != OGRERR_NONE) { ogrEreportError("failure populating OGR feature"); } err = OGR_L_SetFeature(modstate->ogr.lyr, feat); if (err != OGRERR_NONE) { ogrEreportError("failure writing back OGR feature"); } OGR_F_Destroy(feat); /* TODO: slot handling? what happens with RETURNING clauses? */ return slot; } // typedef struct TupleTableSlot // { // NodeTag type; // bool tts_isempty; /* true = slot is empty */ // bool tts_shouldFree; /* should pfree tts_tuple? */ // bool tts_shouldFreeMin; /* should pfree tts_mintuple? */ // bool tts_slow; /* saved state for slot_deform_tuple */ // HeapTuple tts_tuple; /* physical tuple, or NULL if virtual */ // TupleDesc tts_tupleDescriptor; /* slot's tuple descriptor */ // MemoryContext tts_mcxt; /* slot itself is in this context */ // Buffer tts_buffer; /* tuple's buffer, or InvalidBuffer */ // int tts_nvalid; /* # of valid values in tts_values */ // Datum *tts_values; /* current per-attribute values */ // bool *tts_isnull; /* current per-attribute isnull flags */ // MinimalTuple tts_mintuple; /* minimal tuple, or NULL if none */ // HeapTupleData tts_minhdr; /* workspace for minimal-tuple-only case */ // long tts_off; /* saved state for slot_deform_tuple */ // } TupleTableSlot; // typedef struct tupleDesc // { // int natts; /* number of attributes in the tuple */ // Form_pg_attribute *attrs; // /* attrs[N] is a pointer to the description of Attribute Number N+1 */ // TupleConstr *constr; /* constraints, or NULL if none */ // Oid tdtypeid; /* composite type ID for tuple type */ // int32 tdtypmod; /* typmod for tuple type */ // bool tdhasoid; /* tuple has oid attribute in its header */ // int tdrefcount; /* reference count, or -1 if not counting */ // } *TupleDesc; // // typedef struct ResultRelInfo // { // NodeTag type; // Index ri_RangeTableIndex; // Relation ri_RelationDesc; // int ri_NumIndices; // RelationPtr ri_IndexRelationDescs; // IndexInfo **ri_IndexRelationInfo; // TriggerDesc *ri_TrigDesc; // FmgrInfo *ri_TrigFunctions; // List **ri_TrigWhenExprs; // Instrumentation *ri_TrigInstrument; // struct FdwRoutine *ri_FdwRoutine; // void *ri_FdwState; // List *ri_WithCheckOptions; // List *ri_WithCheckOptionExprs; // List **ri_ConstraintExprs; // JunkFilter *ri_junkFilter; // ProjectionInfo *ri_projectReturning; // ProjectionInfo *ri_onConflictSetProj; // List *ri_onConflictSetWhere; // } ResultRelInfo; // typedef struct TargetEntry // { // Expr xpr; // Expr *expr; /* expression to evaluate */ // AttrNumber resno; /* attribute number (see notes above) */ // char *resname; /* name of the column (could be NULL) */ // Index ressortgroupref;/* nonzero if referenced by a sort/group // * clause */ // Oid resorigtbl; /* OID of column's source table */ // AttrNumber resorigcol; /* column's number in source table */ // bool resjunk; /* set to true to eliminate the attribute from // * final target list */ // } TargetEntry; // TargetEntry * // makeTargetEntry(Expr *expr, // AttrNumber resno, // char *resname, // bool resjunk) // Var * // makeVar(Index varno, // AttrNumber varattno, // Oid vartype, // int32 vartypmod, // Oid varcollid, // Index varlevelsup) // typedef struct Var // { // Expr xpr; // Index varno; /* index of this var's relation in the range // * table, or INNER_VAR/OUTER_VAR/INDEX_VAR */ // AttrNumber varattno; /* attribute number of this var, or zero for // * all */ // Oid vartype; /* pg_type OID for the type of this var */ // int32 vartypmod; /* pg_attribute typmod value */ // Oid varcollid; /* OID of collation, or InvalidOid if none */ // Index varlevelsup; /* for subquery variables referencing outer // * relations; 0 in a normal var, >0 means N // * levels up */ // Index varnoold; /* original value of varno, for debugging */ // AttrNumber varoattno; /* original value of varattno */ // int location; /* token location, or -1 if unknown */ // } Var; static TupleTableSlot* ogrExecForeignInsert(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot) { OgrFdwModifyState* modstate = rinfo->ri_FdwState; OGRFeatureDefnH ogr_fd = OGR_L_GetLayerDefn(modstate->ogr.lyr); OGRFeatureH feat = OGR_F_Create(ogr_fd); int fid_column; OGRErr err; GIntBig fid; elog(DEBUG3, "%s: entered function", __func__); #if PG_VERSION_NUM >= 120000 /* * PgSQL 12 passes an unpopulated slot to us, and for now * we force it to populate itself and then read directly * from it. For future, using the slot_getattr() infra * would be cleaner, but version dependent. */ slot_getallattrs(slot); #endif /* Copy the data from the slot onto the feature */ if (!feat) { ogrEreportError("failure creating OGR feature"); } err = ogrSlotToFeature(slot, feat, modstate->table); if (err != OGRERR_NONE) { ogrEreportError("failure populating OGR feature"); } err = OGR_L_CreateFeature(modstate->ogr.lyr, feat); if (err != OGRERR_NONE) { ogrEreportError("failure writing OGR feature"); } fid = OGR_F_GetFID(feat); OGR_F_Destroy(feat); /* Update the FID for RETURNING slot */ fid_column = ogrGetFidColumn(slot->tts_tupleDescriptor); if (fid_column >= 0) { slot->tts_values[fid_column] = Int64GetDatum(fid); slot->tts_isnull[fid_column] = false; slot->tts_nvalid++; } return slot; } static TupleTableSlot* ogrExecForeignDelete(EState* estate, ResultRelInfo* rinfo, TupleTableSlot* slot, TupleTableSlot* planSlot) { OgrFdwModifyState* modstate = rinfo->ri_FdwState; TupleDesc td = planSlot->tts_tupleDescriptor; Relation rel = rinfo->ri_RelationDesc; Oid foreigntableid = RelationGetRelid(rel); int fid_column; Oid fid_type; Datum fid_datum; int64 fid; OGRErr err; elog(DEBUG3, "%s: entered function", __func__); /* Is there a fid column? */ fid_column = ogrGetFidColumn(td); if (fid_column < 0) { elog(ERROR, "cannot find 'fid' column in table '%s'", get_rel_name(foreigntableid)); } #if PG_VERSION_NUM >= 120000 slot_getallattrs(planSlot); #endif /* What is the value of the FID for this record? */ fid_datum = planSlot->tts_values[fid_column]; #if PG_VERSION_NUM >= 110000 fid_type = td->attrs[fid_column].atttypid; #else fid_type = td->attrs[fid_column]->atttypid; #endif if (fid_type == INT8OID) { fid = DatumGetInt64(fid_datum); } else { fid = DatumGetInt32(fid_datum); } elog(DEBUG2, "ogrExecForeignDelete fid=" OGR_FDW_FRMT_INT64, OGR_FDW_CAST_INT64(fid)); /* Delete the OGR feature for this fid */ err = OGR_L_DeleteFeature(modstate->ogr.lyr, fid); if (err != OGRERR_NONE) { return NULL; } else { return slot; } } static void ogrEndForeignModify(EState* estate, ResultRelInfo* rinfo) { OgrFdwModifyState* modstate = rinfo->ri_FdwState; elog(DEBUG3, "%s: entered function", __func__); ogrFinishConnection(&(modstate->ogr)); return; } static int ogrIsForeignRelUpdatable(Relation rel) { const int readonly = 0; int foreign_rel_updateable = 0; TupleDesc td = RelationGetDescr(rel); OgrConnection ogr; Oid foreigntableid = RelationGetRelid(rel); elog(DEBUG3, "%s: entered function", __func__); /* Before we say "yes"... */ /* Does the foreign relation have a "fid" column? */ /* Is that column an integer? */ if (ogrGetFidColumn(td) < 0) { elog(NOTICE, "no \"fid\" column in foreign table '%s'", get_rel_name(foreigntableid)); return readonly; } /* Is it backed by a writable OGR driver? */ /* Can we open the relation in read/write mode? */ ogr = ogrGetConnectionFromTable(foreigntableid, OGR_UPDATEABLE_TRY); /* Something in the open process set the readonly flags */ /* Perhaps user has manually set the foreign table option to readonly */ if (ogr.ds_updateable == OGR_UPDATEABLE_FALSE || ogr.lyr_updateable == OGR_UPDATEABLE_FALSE) { return readonly; } /* No data source or layer objects? Readonly */ if (!(ogr.ds && ogr.lyr)) { return readonly; } if (OGR_L_TestCapability(ogr.lyr, OLCRandomWrite)) { foreign_rel_updateable |= (1 << CMD_UPDATE); } if (OGR_L_TestCapability(ogr.lyr, OLCSequentialWrite)) { foreign_rel_updateable |= (1 << CMD_INSERT); } if (OGR_L_TestCapability(ogr.lyr, OLCDeleteFeature)) { foreign_rel_updateable |= (1 << CMD_DELETE); } ogrFinishConnection(&ogr); return foreign_rel_updateable; } #if PG_VERSION_NUM >= 90500 /* * PostgreSQL 9.5 or above. Import a foreign schema */ static List* ogrImportForeignSchema(ImportForeignSchemaStmt* stmt, Oid serverOid) { List* commands = NIL; ForeignServer* server; ListCell* lc; bool import_all = false; bool launder_column_names, launder_table_names; OgrConnection ogr; int i; char layer_name[STR_MAX_LEN]; char table_name[STR_MAX_LEN]; elog(DEBUG3, "%s: entered function", __func__); /* Are we importing all layers in the OGR datasource? */ import_all = streq(stmt->remote_schema, "ogr_all"); /* Make connection to server */ server = GetForeignServer(serverOid); ogr = ogrGetConnectionFromServer(serverOid, OGR_UPDATEABLE_FALSE); /* Launder by default */ launder_column_names = launder_table_names = true; /* Read user-provided statement laundering options */ foreach (lc, stmt->options) { DefElem* def = (DefElem*) lfirst(lc); if (streq(def->defname, "launder_column_names")) { launder_column_names = defGetBoolean(def); } else if (streq(def->defname, "launder_table_names")) { launder_table_names = defGetBoolean(def); } else ereport(ERROR, (errcode(ERRCODE_FDW_INVALID_OPTION_NAME), errmsg("invalid option \"%s\"", def->defname))); } for (i = 0; i < GDALDatasetGetLayerCount(ogr.ds); i++) { bool import_layer = false; OGRLayerH ogr_lyr = GDALDatasetGetLayer(ogr.ds, i); if (! ogr_lyr) { elog(DEBUG1, "Skipping OGR layer %d, unable to read layer", i); continue; } /* Layer name is never laundered, since it's the link back to OGR */ strncpy(layer_name, OGR_L_GetName(ogr_lyr), STR_MAX_LEN); /* * We need to compare against created table names * because PgSQL does an extra check on CREATE FOREIGN TABLE */ strncpy(table_name, layer_name, STR_MAX_LEN); if (launder_table_names) { ogrStringLaunder(table_name); } /* * Only include if we are importing "ogr_all" or * the layer prefix starts with the remote schema */ import_layer = import_all || (strncmp(layer_name, stmt->remote_schema, strlen(stmt->remote_schema)) == 0); /* Apply restrictions for LIMIT TO and EXCEPT */ if (import_layer && ( stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO || stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT)) { /* Limited list? Assume we are taking no items */ if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO) { import_layer = false; } /* Check the list for our items */ foreach (lc, stmt->table_list) { RangeVar* rv = (RangeVar*) lfirst(lc); /* Found one! */ if (streq(rv->relname, table_name)) { if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO) { import_layer = true; } else { import_layer = false; } break; } } } if (import_layer) { OGRErr err; stringbuffer_t buf; stringbuffer_init(&buf); err = ogrLayerToSQL(ogr_lyr, server->servername, launder_table_names, launder_column_names, NULL, ogrGetGeometryOid() != BYTEAOID, &buf ); if (err != OGRERR_NONE) { elog(ERROR, "unable to generate IMPORT SQL for '%s'", table_name); } commands = lappend(commands, pstrdup(stringbuffer_getstring(&buf))); stringbuffer_release(&buf); } } elog(NOTICE, "Number of tables to be created %d", list_length(commands)); ogrFinishConnection(&ogr); return commands; } #endif /* PostgreSQL 9.5+ for ogrImportForeignSchema */ #endif /* PostgreSQL 9.3+ version check */ pgsql-ogr-fdw-1.1.5/ogr_fdw.control000066400000000000000000000002301463733357200172500ustar00rootroot00000000000000# ogr_fdw extension comment = 'foreign-data wrapper for GIS data access' default_version = '1.1' module_pathname = '$libdir/ogr_fdw' relocatable = true pgsql-ogr-fdw-1.1.5/ogr_fdw.h000066400000000000000000000125561463733357200160350ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw.h * foreign-data wrapper for GIS data access. * * Copyright (c) 2014-2015, Paul Ramsey * *------------------------------------------------------------------------- */ #ifndef _OGR_FDW_H #define _OGR_FDW_H 1 #define OGR_FDW_RELEASE_NAME "1.1" /* * PostgreSQL */ #include "postgres.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/reloptions.h" #include "access/sysattr.h" #include "access/transam.h" #include "catalog/namespace.h" #include "catalog/pg_collation.h" #include "catalog/pg_foreign_table.h" #include "catalog/pg_foreign_server.h" #include "catalog/pg_namespace.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/copy.h" #include "commands/defrem.h" #include "commands/explain.h" #include "commands/vacuum.h" #include "foreign/fdwapi.h" #include "foreign/foreign.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "optimizer/clauses.h" #include "optimizer/cost.h" #include "optimizer/pathnode.h" #include "optimizer/planmain.h" #include "optimizer/restrictinfo.h" #include "parser/parsetree.h" #include "storage/ipc.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/numeric.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/timestamp.h" #if PG_VERSION_NUM < 120000 #include "nodes/relation.h" #include "optimizer/var.h" #else #include "executor/tuptable.h" #include "optimizer/appendinfo.h" #endif #ifdef PACKAGE_URL #undef PACKAGE_URL #endif /* GDAL/OGR includes and compat */ #include "ogr_fdw_gdal.h" #include "ogr_fdw_common.h" /* Local configuration defines */ /* Use hexwkb input by default, but have option to use */ /* the binary recv input instead. Binary input is strict */ /* on geometry structure (no unclosed polys, etc) and */ /* hexwkb is not. */ #define OGR_FDW_HEXWKB TRUE typedef enum { OGR_UNMATCHED, OGR_GEOMETRY, OGR_FID, OGR_FIELD } OgrColumnVariant; typedef enum { OGR_UPDATEABLE_FALSE, OGR_UPDATEABLE_TRUE, OGR_UPDATEABLE_UNSET, OGR_UPDATEABLE_TRY } OgrUpdateable; typedef struct OgrFdwColumn { /* PgSQL metadata */ int pgattnum; /* PgSQL attribute number */ int pgattisdropped; /* PgSQL attribute dropped? */ char* pgname; /* PgSQL column name */ Oid pgtype; /* PgQL data type */ int pgtypmod; /* PgSQL type modifier */ bool pgisarray; Oid pgelmtype; /* If column is array then this is nonzero */ /* For reading. If array, for array element type. */ Oid pginputfunc; /* PgSQL convert cstring to type */ Oid pginputioparam; Oid pgrecvfunc; /* PgSQL convert binary to type */ Oid pgrecvioparam; /* For writing. If array, for array element type. */ Oid pgoutputfunc; /* PgSQL convert type to cstring */ bool pgoutputvarlena; Oid pgsendfunc; /* PgSQL convert type to binary */ bool pgsendvarlena; /* OGR metadata */ OgrColumnVariant ogrvariant; int ogrfldnum; OGRFieldType ogrfldtype; } OgrFdwColumn; typedef struct OgrFdwTable { int ncols; char* tblname; OgrFdwColumn* cols; } OgrFdwTable; typedef struct OgrFdwSpatialFilter { int ogrfldnum; double minx, miny, maxx, maxy; } OgrFdwSpatialFilter; typedef struct OgrConnection { const char* ds_str; /* datasource connection string */ const char* dr_str; /* driver (format) name */ char* lyr_str; /* layer name */ const char* config_options; /* GDAL config options */ const char* open_options; /* GDAL open options */ OgrUpdateable ds_updateable; OgrUpdateable lyr_updateable; int char_encoding; /* Is OGR layer UTF? Has user provided encoding open option? */ GDALDatasetH ds; /* GDAL datasource handle */ OGRLayerH lyr; /* OGR layer handle */ } OgrConnection; typedef enum { OGR_PLAN_STATE, OGR_EXEC_STATE, OGR_MODIFY_STATE } OgrFdwStateType; typedef struct OgrFdwState { OgrFdwStateType type; Oid foreigntableid; OgrConnection ogr; /* connection object */ OgrFdwTable* table; TupleDesc tupdesc; } OgrFdwState; typedef struct OgrFdwPlanState { OgrFdwStateType type; Oid foreigntableid; OgrConnection ogr; OgrFdwTable* table; TupleDesc tupdesc; int nrows; /* estimate of number of rows in file */ Cost startup_cost; Cost total_cost; bool* pushdown_clauses; } OgrFdwPlanState; typedef struct OgrFdwExecState { OgrFdwStateType type; Oid foreigntableid; OgrConnection ogr; OgrFdwTable *table; TupleDesc tupdesc; char* sql; /* OGR SQL for attribute filter */ int rownum; /* how many rows have we read thus far? */ Oid setsridfunc; /* ST_SetSRID() */ Oid typmodsridfunc; /* postgis_typmod_srid() */ } OgrFdwExecState; typedef struct OgrFdwModifyState { OgrFdwStateType type; Oid foreigntableid; OgrConnection ogr; /* connection object */ OgrFdwTable* table; TupleDesc tupdesc; } OgrFdwModifyState; /* Shared function signatures */ bool ogrDeparse(StringInfo buf, PlannerInfo* root, RelOptInfo* foreignrel, List* exprs, OgrFdwState* state, List** params_list, OgrFdwSpatialFilter** sf); Oid ogrGetGeometryOid(void); OGRErr pgDatumToOgrGeometry (Datum pg_geometry, Oid pgsendfunc, OGRGeometryH* ogr_geometry); #endif /* _OGR_FDW_H */ pgsql-ogr-fdw-1.1.5/ogr_fdw_common.c000066400000000000000000000234521463733357200173750ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw_common.c * foreign-data wrapper for GIS data access. * * Copyright (c) 2014-2016, Paul Ramsey * *------------------------------------------------------------------------- */ #include "ogr_fdw_gdal.h" #include "ogr_fdw_common.h" #include "stringbuffer.h" #include "pg_config_manual.h" /* Prototype for function that must be defined in PostgreSQL (it is) */ /* and in ogr_fdw_info (it is) */ const char * quote_identifier(const char *ident); /* * Append a SQL string literal representing "val" to buf. */ static void ogrDeparseStringLiteral(stringbuffer_t *buf, const char *val) { const char *valptr; /* * Rather than making assumptions about the remote server's value of * standard_conforming_strings, always use E'foo' syntax if there are any * backslashes. This will fail on remote servers before 8.1, but those * are long out of support. */ if ( strchr(val, '\\') != NULL ) { stringbuffer_append_char(buf, 'E'); } stringbuffer_append_char(buf, '\''); for ( valptr = val; *valptr; valptr++ ) { char ch = *valptr; if ( ch == '\'' || ch == '\\' ) { stringbuffer_append_char(buf, ch); } stringbuffer_append_char(buf, ch); } stringbuffer_append_char(buf, '\''); } void ogrStringLaunder(char *str) { int i, j = 0; char tmp[NAMEDATALEN]; memset(tmp, 0, NAMEDATALEN); for(i = 0; str[i]; i++) { char c = tolower(str[i]); /* First character is a numeral, prefix with 'n' */ if ( i == 0 && (c >= 48 && c <= 57) ) { tmp[j++] = 'n'; } /* Replace non-safe characters w/ _ */ if ( (c >= 48 && c <= 57) || /* 0-9 */ (c >= 65 && c <= 90) || /* A-Z */ (c >= 97 && c <= 122) /* a-z */ ) { /* Good character, do nothing */ } else { c = '_'; } tmp[j++] = c; /* Avoid mucking with data beyond the end of our stack-allocated strings */ if ( j >= NAMEDATALEN - 1) break; } strncpy(str, tmp, NAMEDATALEN); } static void ogrTypeToPgType(OGRFieldDefnH ogr_fld, char *pgtype, size_t width) { OGRFieldType ogr_type = OGR_Fld_GetType(ogr_fld); switch(ogr_type) { case OFTInteger: #if GDAL_VERSION_MAJOR >= 2 if( OGR_Fld_GetSubType(ogr_fld) == OFSTBoolean ) { snprintf(pgtype, width, "boolean"); break; } else #endif snprintf(pgtype, width, "integer"); break; case OFTReal: snprintf(pgtype, width, "double precision"); break; case OFTString: { int ogr_fld_width = OGR_Fld_GetWidth(ogr_fld); if (ogr_fld_width > 0) snprintf(pgtype, width, "varchar(%d)", ogr_fld_width); else snprintf(pgtype, width, "varchar"); break; } case OFTBinary: snprintf(pgtype, width, "bytea"); break; case OFTDate: snprintf(pgtype, width, "date"); break; case OFTTime: snprintf(pgtype, width, "time"); break; case OFTDateTime: snprintf(pgtype, width, "timestamp"); break; case OFTIntegerList: snprintf(pgtype, width, "integer[]"); break; case OFTRealList: snprintf(pgtype, width, "double precision[]"); break; case OFTStringList: snprintf(pgtype, width, "varchar[]"); break; #if GDAL_VERSION_MAJOR >= 2 case OFTInteger64: snprintf(pgtype, width, "bigint"); break; case OFTInteger64List: snprintf(pgtype, width, "bigint[]"); break; #endif default: CPLError(CE_Failure, CPLE_AssertionFailed, "unsupported GDAL type '%s'", OGR_GetFieldTypeName(ogr_type)); return; } return; } static void ogrGeomTypeToPgGeomType(stringbuffer_t *buf, OGRwkbGeometryType gtype) { switch(wkbFlatten(gtype)) { case wkbUnknown: stringbuffer_append(buf, "Geometry"); break; case wkbPoint: stringbuffer_append(buf, "Point"); break; case wkbLineString: stringbuffer_append(buf, "LineString"); break; case wkbPolygon: stringbuffer_append(buf, "Polygon"); break; case wkbMultiPoint: stringbuffer_append(buf, "MultiPoint"); break; case wkbMultiLineString: stringbuffer_append(buf, "MultiLineString"); break; case wkbMultiPolygon: stringbuffer_append(buf, "MultiPolygon"); break; case wkbGeometryCollection: stringbuffer_append(buf, "GeometryCollection"); break; #if GDAL_VERSION_MAJOR >= 2 case wkbCircularString: stringbuffer_append(buf, "CircularString"); break; case wkbCompoundCurve: stringbuffer_append(buf, "CompoundCurve"); break; case wkbCurvePolygon: stringbuffer_append(buf, "CurvePolygon"); break; case wkbMultiCurve: stringbuffer_append(buf, "MultiCurve"); break; case wkbMultiSurface: stringbuffer_append(buf, "MultiSurface"); break; #endif case wkbNone: CPLError(CE_Failure, CPLE_AssertionFailed, "Cannot handle OGR geometry type wkbNone"); break; default: CPLError(CE_Failure, CPLE_AssertionFailed, "Cannot handle OGR geometry type '%d'", gtype); } #if GDAL_VERSION_MAJOR >= 2 if ( wkbHasZ(gtype) ) #else if ( gtype & wkb25DBit ) #endif stringbuffer_append(buf, "Z"); #if GDAL_VERSION_MAJOR >= 2 && GDAL_VERSION_MINOR >= 1 if ( wkbHasM(gtype) ) stringbuffer_append(buf, "M"); #endif return; } static OGRErr ogrColumnNameToSQL (const char *ogrcolname, const char *pgtype, int launder_column_names, stringbuffer_t *buf) { char pgcolname[NAMEDATALEN]; strncpy(pgcolname, ogrcolname, NAMEDATALEN); ogrStringLaunder(pgcolname); if ( launder_column_names ) { stringbuffer_aprintf(buf, ",\n %s %s", quote_identifier(pgcolname), pgtype); if ( ! strcaseeq(pgcolname, ogrcolname) ) { stringbuffer_append(buf, " OPTIONS (column_name "); ogrDeparseStringLiteral(buf, ogrcolname); stringbuffer_append(buf, ")"); } } else { /* OGR column is PgSQL compliant, we're all good */ if ( streq(pgcolname, ogrcolname) ) stringbuffer_aprintf(buf, ",\n %s %s", quote_identifier(ogrcolname), pgtype); /* OGR is mixed case or non-compliant, we need to quote it */ else stringbuffer_aprintf(buf, ",\n \"%s\" %s", ogrcolname, pgtype); } return OGRERR_NONE; } OGRErr ogrLayerToSQL (const OGRLayerH ogr_lyr, const char *fdw_server, int launder_table_names, int launder_column_names, const char *fdw_table_name, int use_postgis_geometry, stringbuffer_t *buf) { int geom_field_count, i; char table_name[NAMEDATALEN]; OGRFeatureDefnH ogr_fd = OGR_L_GetLayerDefn(ogr_lyr); stringbuffer_t gbuf; stringbuffer_init(&gbuf); if ( ! ogr_fd ) { CPLError(CE_Failure, CPLE_AssertionFailed, "unable to get OGRFeatureDefnH from OGRLayerH"); return OGRERR_FAILURE; } #if GDAL_VERSION_MAJOR >= 2 || GDAL_VERSION_MINOR >= 11 geom_field_count = OGR_FD_GetGeomFieldCount(ogr_fd); #else geom_field_count = (OGR_L_GetGeomType(ogr_lyr) != wkbNone); #endif /* Process table name */ if (fdw_table_name == NULL) { strncpy(table_name, OGR_L_GetName(ogr_lyr), NAMEDATALEN); if (launder_table_names) ogrStringLaunder(table_name); } else { strncpy(table_name, fdw_table_name, NAMEDATALEN); } /* Create table */ stringbuffer_aprintf(buf, "CREATE FOREIGN TABLE %s (\n", quote_identifier(table_name)); /* For now, every table we auto-create will have a FID */ stringbuffer_append(buf, " fid bigint"); /* Handle all geometry columns in the OGR source */ for ( i = 0; i < geom_field_count; i++ ) { int srid = 0; #if GDAL_VERSION_MAJOR >= 2 || GDAL_VERSION_MINOR >= 11 OGRGeomFieldDefnH gfld = OGR_FD_GetGeomFieldDefn(ogr_fd, i); OGRwkbGeometryType gtype = OGR_GFld_GetType(gfld); const char *geomfldname = OGR_GFld_GetNameRef(gfld); OGRSpatialReferenceH gsrs = OGR_GFld_GetSpatialRef(gfld); #else OGRwkbGeometryType gtype = OGR_FD_GetGeomType(ogr_fd); const char *geomfldname = "geom"; OGRSpatialReferenceH gsrs = OGR_L_GetSpatialRef(ogr_lyr); #endif /* Skip geometry type we cannot handle */ if ( gtype == wkbNone ) continue; /* Clear out our geometry type buffer */ stringbuffer_clear(&gbuf); /* PostGIS geometry type has lots of complex stuff */ if ( use_postgis_geometry ) { /* Add geometry type info */ stringbuffer_append(&gbuf, "Geometry("); ogrGeomTypeToPgGeomType(&gbuf, gtype); /* See if we have an EPSG code to work with */ if ( gsrs ) { const char *charAuthType; const char *charSrsCode; OSRAutoIdentifyEPSG(gsrs); charAuthType = OSRGetAttrValue(gsrs, "AUTHORITY", 0); charSrsCode = OSRGetAttrValue(gsrs, "AUTHORITY", 1); if ( charAuthType && strcaseeq(charAuthType, "EPSG") && charSrsCode && atoi(charSrsCode) > 0 ) { srid = atoi(charSrsCode); } } /* Add EPSG number, if figured it out */ if ( srid ) { stringbuffer_aprintf(&gbuf, ",%d)", srid); } else { stringbuffer_append(&gbuf, ")"); } } /* Bytea is simple */ else { stringbuffer_append(&gbuf, "bytea"); } /* Use geom field name if we have it */ if ( geomfldname && strlen(geomfldname) > 0 ) { ogrColumnNameToSQL(geomfldname, stringbuffer_getstring(&gbuf), launder_column_names, buf); } /* Or a numbered generic name if we don't */ else if ( geom_field_count > 1 ) { stringbuffer_aprintf(buf, ",\n geom%d %s", i, stringbuffer_getstring(&gbuf)); } /* Or just a generic name */ else { stringbuffer_aprintf(buf, ",\n geom %s", stringbuffer_getstring(&gbuf)); } } /* Write out attribute fields */ for ( i = 0; i < OGR_FD_GetFieldCount(ogr_fd); i++ ) { char pgtype[NAMEDATALEN]; OGRFieldDefnH ogr_fld = OGR_FD_GetFieldDefn(ogr_fd, i); ogrTypeToPgType(ogr_fld, pgtype, sizeof(pgtype)); ogrColumnNameToSQL(OGR_Fld_GetNameRef(ogr_fld), pgtype, launder_column_names, buf); } /* * Add server name and layer-level options. We specify remote * layer name as option */ stringbuffer_aprintf(buf, "\n) SERVER %s\nOPTIONS (", quote_identifier(fdw_server)); stringbuffer_append(buf, "layer "); ogrDeparseStringLiteral(buf, OGR_L_GetName(ogr_lyr)); stringbuffer_append(buf, ");\n"); return OGRERR_NONE; } pgsql-ogr-fdw-1.1.5/ogr_fdw_common.h000066400000000000000000000017211463733357200173750ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw_common.h * foreign-data wrapper for GIS data access. * * Copyright (c) 2014-2015, Paul Ramsey * *------------------------------------------------------------------------- */ #ifndef _OGR_FDW_COMMON_H #define _OGR_FDW_COMMON_H 1 #include #include #include "stringbuffer.h" #define STR_MAX_LEN 256 /* Utility macros for string equality */ #define streq(s1,s2) (strcmp((s1),(s2)) == 0) #define strcaseeq(s1,s2) (strcasecmp((s1),(s2)) == 0) /* Re-write a string in place with laundering rules */ void ogrStringLaunder(char *str); OGRErr ogrLayerToSQL (const OGRLayerH ogr_lyr, const char *fwd_server, int launder_table_names, int launder_column_names, const char *fdw_table_name, int use_postgis_geometry, stringbuffer_t *buf); #endif /* _OGR_FDW_COMMON_H */ pgsql-ogr-fdw-1.1.5/ogr_fdw_deparse.c000066400000000000000000000411761463733357200175330ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw_deparse.c * foreign-data wrapper for GIS data access. * * Copyright (c) 2014-2015, Paul Ramsey * * Convert parse tree to a QueryExpression as described at * http://gdal.org/ogr_sql.html *------------------------------------------------------------------------- */ /* * Local structures */ #include "ogr_fdw.h" typedef struct OgrDeparseCtx { PlannerInfo* root; /* global planner state */ RelOptInfo* foreignrel; /* the foreign relation we are planning for */ StringInfo buf; /* output buffer to append to */ List** params_list; /* exprs that will become remote Params */ OgrFdwSpatialFilter* spatial_filter; /* spatial filter bounds and fieldnumber */ OgrFdwState* state; /* to convert local column names to OGR names */ } OgrDeparseCtx; /* Local function signatures */ static bool ogrDeparseExpr(Expr* node, OgrDeparseCtx* context); // static void ogrDeparseOpExpr(OpExpr* node, OgrDeparseCtx *context); static void setStringInfoLength(StringInfo str, int len) { str->len = len; str->data[len] = '\0'; } static void stringInfoReverse(StringInfo str, unsigned int len) { if (str->len > len) str->len -= len; } static char* ogrStringFromDatum(Datum datum, Oid type) { StringInfoData result; regproc typoutput; HeapTuple tuple; char* str, *p; /* Special handling for boolean */ if (type == BOOLOID) { if (datum) { return "1=1"; } else { return "1=0"; } } /* get the type's output function */ tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type)); if (!HeapTupleIsValid(tuple)) { elog(ERROR, "cache lookup failed for type %u", type); } typoutput = ((Form_pg_type)GETSTRUCT(tuple))->typoutput; ReleaseSysCache(tuple); initStringInfo(&result); /* Special handling to convert a geometry to a bbox needed here */ if (type == ogrGetGeometryOid()) { elog(ERROR, "got a GEOMETRY!"); return NULL; } /* render the constant in OGR SQL */ switch (type) { case TEXTOID: case DATEOID: case TIMESTAMPOID: case TIMESTAMPTZOID: case CHAROID: case BPCHAROID: case VARCHAROID: case NAMEOID: str = DatumGetCString(OidFunctionCall1(typoutput, datum)); /* Don't return a zero length string, return an empty string */ if (str[0] == '\0') { return "''"; } /* wrap string with ' */ appendStringInfoChar(&result, '\''); for (p = str; *p; ++p) { /* Escape single quotes as doubled '' */ if (*p == '\'') { appendStringInfoChar(&result, '\''); } appendStringInfoChar(&result, *p); } appendStringInfoChar(&result, '\''); break; case INT8OID: case INT2OID: case INT4OID: case OIDOID: case FLOAT4OID: case FLOAT8OID: case NUMERICOID: appendStringInfoString(&result, DatumGetCString(OidFunctionCall1(typoutput, datum))); break; default: elog(DEBUG1, "could not convert type (%d) to OGR query form", type); return NULL; } return result.data; } static bool ogrDeparseConst(Const* constant, OgrDeparseCtx* context) { /* TODO: Can OGR do anythign w/ NULL? */ if (constant->constisnull) { appendStringInfoString(context->buf, "NULL"); } /* Use geometry as a spatial filter? */ else if (constant->consttype == ogrGetGeometryOid()) { /* * For geometry we need to convert the gserialized constant into * an OGRGeometry for the OGR spatial filter. * For that, we can use the type's "send" function * which takes in gserialized and spits out EWKB. */ Oid sendfunction; bool typeIsVarlena; Datum wkbdatum; char* gser; char* wkb; char* wkt; int wkb_size; OGRGeometryH ogrgeom; /* * Given a type oid (geometry in this case), * look up the "send" function that takes in * serialized input and outputs the binary (WKB) form. */ getTypeBinaryOutputInfo(constant->consttype, &sendfunction, &typeIsVarlena); wkbdatum = OidFunctionCall1(sendfunction, constant->constvalue); /* * Convert the WKB into an OGR geometry */ gser = DatumGetPointer(wkbdatum); wkb = VARDATA(gser); wkb_size = VARSIZE(gser) - VARHDRSZ; OGR_G_CreateFromWkb((unsigned char*)wkb, NULL, &ogrgeom, wkb_size); OGR_G_ExportToWkt(ogrgeom, &wkt); elog(DEBUG1, "ogrDeparseConst got a geometry: %s", wkt); free(wkt); OGR_G_DestroyGeometry(ogrgeom); /* * geometry doesn't play a role in the deparsed SQL */ return false; } else { /* get a string representation of the value */ char* c = ogrStringFromDatum(constant->constvalue, constant->consttype); if (c == NULL) { return false; } else { appendStringInfoString(context->buf, c); } } return true; } static bool ogrDeparseParam(Param* node, OgrDeparseCtx* context) { elog(DEBUG3, "got into ogrDeparseParam code"); return false; } static bool ogrIsLegalVarName(const char* varname) { size_t len = strlen(varname); int i; for (i = 0; i < len; i++) { char c = varname[i]; /* First char must be a-zA-Z */ if (i == 0 && !((c >= 97 && c <= 122) || (c >= 65 && c <= 90))) { return false; } /* All other chars must be 0-9a-zA-Z_ */ if (!((c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 59) || (c == 96))) { return false; } } return true; } static bool ogrDeparseVarOgrColumn(const Var* node, const OgrDeparseCtx* context, OgrFdwColumn *col) { /* Var belongs to foreign table */ int i; OgrFdwTable* table = context->state->table; for (i = 0; i < table->ncols; i++) { if (table->cols[i].pgattnum == node->varattno) { *col = table->cols[i]; return true; } } return false; } static const char * ogrDeparseVarName(const Var* node, const OgrDeparseCtx* context) { /* Var belongs to foreign table */ OGRLayerH lyr = context->state->ogr.lyr; OgrFdwColumn col; if (ogrDeparseVarOgrColumn(node, context, &col)) { const char* fldname = NULL; if (col.ogrvariant == OGR_FID) { fldname = OGR_L_GetFIDColumn(lyr); if (! fldname || strlen(fldname) == 0) { fldname = "fid"; } } else if (col.ogrvariant == OGR_FIELD) { OGRFeatureDefnH fd = OGR_L_GetLayerDefn(lyr); OGRFieldDefnH fld = OGR_FD_GetFieldDefn(fd, col.ogrfldnum); fldname = OGR_Fld_GetNameRef(fld); } if (fldname) return fldname; } return NULL; } static bool ogrDeparseVar(const Var* node, OgrDeparseCtx* context) { StringInfoData* buf = context->buf; /* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */ Assert(!IS_SPECIAL_VARNO(node->varno)); if (node->varno == context->foreignrel->relid && node->varlevelsup == 0) { const char* fldname = ogrDeparseVarName(node, context); if (fldname) { if (ogrIsLegalVarName(fldname)) { appendStringInfoString(buf, fldname); } else { appendStringInfo(buf, "\"%s\"", fldname); } } else { return false; } } else { elog(ERROR, "got to param handling section of ogrDeparseVar"); return false; } return true; } static int ogrOperatorCmpFunc(const void* a, const void* b) { return strcasecmp(*(const char**)a, *(const char**)b); } static bool ogrOperatorIsSupported(const char* opname) { /* IMPORTANT */ /* This array MUST be in sorted order or the bsearch will fail */ static const char* ogrOperators[10] = { "!=", "&&", "<", "<=", "<>", "=", ">", ">=", "~~", "~~*" }; elog(DEBUG3, "ogrOperatorIsSupported got operator '%s'", opname); return NULL != bsearch(&opname, ogrOperators, 10, sizeof(char*), ogrOperatorCmpFunc); } static bool ogrDeparseOpExprSpatial(OpExpr* node, OgrDeparseCtx* context) { Expr* r_arg = lfirst(list_head(node->args)); Expr* l_arg = lfirst(list_tail(node->args)); Expr* exprconst = NULL; Const* constant = NULL; Var* var = NULL; OgrFdwColumn col; OGRLayerH lyr; OGRFeatureDefnH fdh; OGRGeomFieldDefnH gfdh; OGRGeometryH geom; OGREnvelope env; OGRErr err; const char* fldname; elog(DEBUG4, "%s:%d entered ogrDeparseOpExprSpatial", __FILE__, __LINE__); /* We need a Geometry T_Const on one side and a T_Var */ /* column on the other side that is from the FDW relation */ /* Both of those implies and OGR spatial filter can be reasonably */ /* set. */ if (nodeTag(l_arg) == T_Var) { var = (Var*)l_arg; exprconst = r_arg; } else if (nodeTag(r_arg) == T_Var) { var = (Var*)r_arg; exprconst = l_arg; } else return false; if (nodeTag(exprconst) == T_Const) { constant = (Const*)exprconst; } else return false; /* Const isn't a geometry type? Done. */ if (constant->consttype != ogrGetGeometryOid() || constant->constisnull || constant->constbyval) return false; /* Var doesn't match an OGR field? Done. */ if (!ogrDeparseVarOgrColumn(var, context, &col)) return false; /* Matched field isn't an OGR geometry? Done. */ if (col.ogrvariant != OGR_GEOMETRY) return false; lyr = context->state->ogr.lyr; fdh = OGR_L_GetLayerDefn(lyr); gfdh = OGR_FD_GetGeomFieldDefn(fdh, col.ogrfldnum); fldname = OGR_GFld_GetNameRef(gfdh); elog(DEBUG4, "%s:%d geometry fieldname '%s'", __FILE__, __LINE__, fldname); err = pgDatumToOgrGeometry (constant->constvalue, col.pgsendfunc, &geom); if (err != OGRERR_NONE) return false; elog(DEBUG4, "%s:%d geometry constant is %s", __FILE__, __LINE__, OGR_G_ExportToJson(geom)); OGR_G_GetEnvelope(geom, &env); OGR_G_DestroyGeometry(geom); context->spatial_filter = palloc(sizeof(OgrFdwSpatialFilter)); context->spatial_filter->minx = env.MinX; context->spatial_filter->maxx = env.MaxX; context->spatial_filter->miny = env.MinY; context->spatial_filter->maxy = env.MaxY; context->spatial_filter->ogrfldnum = col.ogrfldnum; elog(DEBUG4, "%s:%d OGR spatial filter is (%f %f, %f %f)", __FILE__, __LINE__, env.MinX, env.MinY, env.MaxX, env.MaxY); return false; } static bool ogrDeparseOpExpr(OpExpr* node, OgrDeparseCtx* context) { StringInfo buf = context->buf; HeapTuple tuple; Form_pg_operator form; char oprkind; char* opname; ListCell* arg; bool result = true; /* Retrieve information about the operator from system catalog. */ tuple = SearchSysCache1(OPEROID, ObjectIdGetDatum(node->opno)); if (!HeapTupleIsValid(tuple)) { elog(ERROR, "cache lookup failed for operator %u", node->opno); } form = (Form_pg_operator) GETSTRUCT(tuple); oprkind = form->oprkind; opname = NameStr(form->oprname); /* Don't deparse expressions we cannot support */ if (! ogrOperatorIsSupported(opname)) { ReleaseSysCache(tuple); return false; } /* Overlaps operator is special case: if one side is a */ /* constant (T_Const), and the other is a table column (T_Var), */ /* then we can pass it as a spatial filter to OGR */ if (strcmp("&&", opname) == 0) { ReleaseSysCache(tuple); return ogrDeparseOpExprSpatial(node, context); } /* Sanity check. */ Assert((oprkind == 'r' && list_length(node->args) == 1) || (oprkind == 'l' && list_length(node->args) == 1) || (oprkind == 'b' && list_length(node->args) == 2)); /* Always parenthesize the operator expression. */ appendStringInfoChar(buf, '('); /* Deparse left operand. */ if (oprkind == 'r' || oprkind == 'b') { arg = list_head(node->args); /* recurse for nested operations */ result &= ogrDeparseExpr(lfirst(arg), context); appendStringInfoChar(buf, ' '); } /* Special case, the 'LIKE' operator is converted to ~~ */ /* by PgSQL, so we have to convert it back here */ /* All OGR string comparisons are case insensitive, so we just */ /* use 'ILIKE' all the time. */ if (streq(opname, "~~") || streq(opname, "~~*")) { opname = "ILIKE"; } /* Operator symbol */ appendStringInfoString(buf, opname); /* Deparse right operand. */ if (oprkind == 'l' || oprkind == 'b') { arg = list_tail(node->args); appendStringInfoChar(buf, ' '); /* recurse for nested operations */ result &= ogrDeparseExpr(lfirst(arg), context); } appendStringInfoChar(buf, ')'); ReleaseSysCache(tuple); return result; } static bool ogrDeparseBoolExpr(BoolExpr* node, OgrDeparseCtx* context) { const char* op = NULL; /* keep compiler quiet */ ListCell* lc; bool first = true; bool result = true; int len_save_all, len_save_part; int boolop = node->boolop; int result_total = 0; StringInfo buf = context->buf; switch (boolop) { case AND_EXPR: op = "AND"; break; case OR_EXPR: op = "OR"; break; /* OGR SQL cannot handle "NOT" */ case NOT_EXPR: return false; } len_save_all = buf->len; appendStringInfoChar(buf, '('); foreach (lc, node->args) { len_save_part = buf->len; /* Connect expressions and parenthesize each condition */ if (! first) { appendStringInfo(buf, " %s ", op); } /* Unparse the expression, if possible */ result = ogrDeparseExpr((Expr*) lfirst(lc), context); result_total += result; /* We can backtrack just this term for AND expressions */ if (boolop == AND_EXPR && ! result) { setStringInfoLength(buf, len_save_part); } /* We have to drop the whole thing if we can't get every part of an OR expression */ if (boolop == OR_EXPR && ! result) { break; } /* Don't flip the "first" bit until we get a good expression */ if (first && result) { first = false; } } appendStringInfoChar(buf, ')'); /* We have to drop the whole thing if we can't get every part of an OR expression */ if (boolop == OR_EXPR && ! result) { setStringInfoLength(buf, len_save_all); } return result_total > 0; } static bool ogrDeparseRelabelType(RelabelType* node, OgrDeparseCtx* context) { if (node->relabelformat != COERCE_IMPLICIT_CAST) { elog(WARNING, "Received a non-implicit relabel expression but did not handle it"); } return ogrDeparseExpr(node->arg, context); } static bool ogrDeparseNullTest(NullTest* node, OgrDeparseCtx* context) { StringInfo buf = context->buf; /* Only push down simple "col IS NULL" tests */ if (nodeTag(node->arg) != T_Var) return false; appendStringInfoString(buf, "("); if(!ogrDeparseVar((Var*)(node->arg), context)) { stringInfoReverse(buf, 1); return false; } if (node->nulltesttype == IS_NULL) { appendStringInfoString(buf, " IS NULL)"); } else { appendStringInfoString(buf, " IS NOT NULL)"); } return true; } static bool ogrDeparseExpr(Expr* node, OgrDeparseCtx* context) { if (node == NULL) { return false; } switch (nodeTag(node)) { case T_OpExpr: return ogrDeparseOpExpr((OpExpr*) node, context); case T_Const: return ogrDeparseConst((Const*) node, context); case T_Var: return ogrDeparseVar((Var*) node, context); case T_Param: return ogrDeparseParam((Param*) node, context); case T_BoolExpr: /* Handle "OR" and "NOT" queries */ return ogrDeparseBoolExpr((BoolExpr*) node, context); case T_NullTest: /* Handle "IS NULL" queries */ return ogrDeparseNullTest((NullTest*) node, context); case T_RelabelType: return ogrDeparseRelabelType((RelabelType*) node, context); case T_ScalarArrayOpExpr: /* TODO: Handle this to support the "IN" operator */ elog(DEBUG2, "unsupported OGR FDW expression type, T_ScalarArrayOpExpr"); return false; #if PG_VERSION_NUM < 120000 case T_ArrayRef: elog(DEBUG2, "unsupported OGR FDW expression type, T_ArrayRef"); return false; #else case T_SubscriptingRef: elog(DEBUG2, "unsupported OGR FDW expression type, T_SubscriptingRef"); return false; #endif case T_ArrayExpr: elog(DEBUG2, "unsupported OGR FDW expression type, T_ArrayExpr"); return false; case T_FuncExpr: elog(DEBUG2, "unsupported OGR FDW expression type, T_FuncExpr"); return false; case T_DistinctExpr: elog(DEBUG2, "unsupported OGR FDW expression type, T_DistinctExpr"); return false; default: elog(DEBUG2, "unsupported OGR FDW expression type for deparse: %d", (int) nodeTag(node)); return false; } } bool ogrDeparse(StringInfo buf, PlannerInfo* root, RelOptInfo* foreignrel, List* exprs, OgrFdwState* state, List** params_list, OgrFdwSpatialFilter** sf) { OgrDeparseCtx context; ListCell* lc; bool first = true; /* initialize result list to empty */ if (params_list) { *params_list = NIL; } /* Set up context struct for recursion */ memset(&context, 0, sizeof(OgrDeparseCtx)); context.buf = buf; context.root = root; context.foreignrel = foreignrel; context.params_list = params_list; context.state = state; context.spatial_filter = NULL; foreach (lc, exprs) { RestrictInfo* ri = (RestrictInfo*) lfirst(lc); int len_save = buf->len; bool result; /* Connect expressions with "AND" and parenthesize each condition */ if (! first) { appendStringInfoString(buf, " AND "); } /* Unparse the expression, if possible */ result = ogrDeparseExpr(ri->clause, &context); if (! result) { /* Couldn't unparse some portion of the expression, so rewind the stringinfo */ setStringInfoLength(buf, len_save); } /* Don't flip the "first" bit until we get a good expression */ if (first && result) { first = false; } } if (context.spatial_filter) *sf = context.spatial_filter; return true; } pgsql-ogr-fdw-1.1.5/ogr_fdw_func.c000066400000000000000000000042011463733357200170270ustar00rootroot00000000000000 /*------------------------------------------------------------------------- * * ogr_fdw_func.c * Helper functions for OGR FDW * * Copyright (c) 2020, Paul Ramsey * *------------------------------------------------------------------------- */ #include "ogr_fdw.h" #include "ogr_fdw_gdal.h" #include #include #include // #include #include #include /** */ Datum ogr_fdw_drivers(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(ogr_fdw_drivers); Datum ogr_fdw_drivers(PG_FUNCTION_ARGS) { /* Array building */ size_t arr_nelems = 0; Datum *arr_elems; ArrayType *arr; Oid elem_type = TEXTOID; int16 elem_len; bool elem_byval; char elem_align; int num_drivers; int i; if (GDALGetDriverCount() <= 0) GDALAllRegister(); #if (GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(1,11,0)) num_drivers = GDALGetDriverCount(); #else num_drivers = OGRGetDriverCount(); #endif if (num_drivers < 1) PG_RETURN_NULL(); arr_elems = palloc0(num_drivers * sizeof(Datum)); get_typlenbyvalalign(elem_type, &elem_len, &elem_byval, &elem_align); for (i = 0; i < num_drivers; i++) { #if GDAL_VERSION_MAJOR <= 1 OGRSFDriverH hDriver = OGRGetDriver(i); text *txtName = cstring_to_text(OGR_Dr_GetName(hDriver)); arr_elems[arr_nelems++] = PointerGetDatum(txtName); #else GDALDriverH hDriver = GDALGetDriver(i); if (GDALGetMetadataItem(hDriver, GDAL_DCAP_VECTOR, NULL) != NULL) { const char *strName = OGR_Dr_GetName(hDriver); text *txtName = cstring_to_text(strName); arr_elems[arr_nelems++] = PointerGetDatum(txtName); } #endif } arr = construct_array(arr_elems, arr_nelems, elem_type, elem_len, elem_byval, elem_align); PG_RETURN_ARRAYTYPE_P(arr); } /** */ Datum ogr_fdw_version(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(ogr_fdw_version); Datum ogr_fdw_version(PG_FUNCTION_ARGS) { const char *gdal_ver = GDAL_RELEASE_NAME; const char *ogr_fdw_ver = OGR_FDW_RELEASE_NAME; char ver_str[256]; snprintf(ver_str, sizeof(ver_str), "OGR_FDW=\"%s\" GDAL=\"%s\"", ogr_fdw_ver, gdal_ver); PG_RETURN_TEXT_P(cstring_to_text(ver_str)); } pgsql-ogr-fdw-1.1.5/ogr_fdw_gdal.h000066400000000000000000000032271463733357200170170ustar00rootroot00000000000000 #ifndef _OGR_FDW_GDAL_H #define _OGR_FDW_GDAL_H 1 /* * Quiet warnings due to double use of * pkgconfig macros in GDAL and PgSQL */ #ifdef PACKAGE_VERSION #undef PACKAGE_VERSION #endif #ifdef PACKAGE_TARNAME #undef PACKAGE_TARNAME #endif #ifdef PACKAGE_STRING #undef PACKAGE_STRING #endif #ifdef PACKAGE_NAME #undef PACKAGE_NAME #endif #ifdef PACKAGE_BUGREPORT #undef PACKAGE_BUGREPORT #endif #ifdef PACKAGE_VERSION #undef PACKAGE_VERSION #endif /* * OGR library API */ #include "gdal.h" #include "ogr_api.h" #include "ogr_srs_api.h" #include "cpl_error.h" #include "cpl_string.h" /* * As far as possible code is GDAL2 compliant, and these * mappings are used to convert to GDAL1-style function * names. For GDALDatasetH opening, there are specific * code blocks to handle version differences between * GDALOpenEx() and OGROpen() */ #if GDAL_VERSION_MAJOR < 2 /* Redefine variable types */ #define GDALDatasetH OGRDataSourceH #define GDALDriverH OGRSFDriverH /* Rename GDAL2 functions to OGR equivs */ #define GDALGetDriverCount() OGRGetDriverCount() #define GDALGetDriver(i) OGRGetDriver(i) #define GDALAllRegister() OGRRegisterAll() #define GDALGetDriverByName(name) OGRGetDriverByName(name) #define GDALClose(ds) OGR_DS_Destroy(ds) #define GDALDatasetGetLayerByName(ds,name) OGR_DS_GetLayerByName(ds,name) #define GDALDatasetGetLayerCount(ds) OGR_DS_GetLayerCount(ds) #define GDALDatasetGetLayer(ds,i) OGR_DS_GetLayer(ds,i) #define GDALGetDriverShortName(dr) OGR_Dr_GetName(dr) #define GDALGetDatasetDriver(ds) OGR_DS_GetDriver(ds) #define GDALDatasetTestCapability(ds,cap) OGR_Dr_TestCapability(ds,cap) #endif /* GDAL 1 support */ #endif /* _OGR_FDW_GDAL_H */ pgsql-ogr-fdw-1.1.5/ogr_fdw_info.c000066400000000000000000000252651463733357200170440ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ogr_fdw_info.c * Commandline utility to read an OGR layer and output a * SQL "create table" statement. * * Copyright (c) 2014-2015, Paul Ramsey * *------------------------------------------------------------------------- */ /* postgresql */ #include "pg_config_manual.h" /* getopt */ #include /* * OGR library API */ #include "ogr_fdw_gdal.h" #include "ogr_fdw_common.h" static void usage(); static OGRErr ogrListLayers(const char* source); static OGRErr ogrFindLayer(const char* source, int layerno, const char** layer); static OGRErr ogrGenerateSQL(const char* server, const char* layer, const char* table, const char* source, const char* options); static int reserved_word(const char* pgcolumn); static char * ogr_fdw_strupr(char* str) { int i; for (i = 0; i < strlen(str); i++) { str[i] = toupper(str[i]); } return str; } static char * strip_spaces(char* str) { unsigned char *cur = (unsigned char *)str; unsigned char *head = cur; while (*head != '\0') { if (*head != ' ') { *cur = *head; ++cur; } ++head; } *cur = '\0'; return str; } /* Define this no-op here, so that code */ /* in the ogr_fdw_common module works */ const char* quote_identifier(const char* ident); char identifier[NAMEDATALEN+3]; const char* quote_identifier(const char* ident) { int len = (int)MIN(strlen(ident), NAMEDATALEN - 1); if (reserved_word(ident)) { sprintf(identifier,"\"%*s\"", len, ident); } else { sprintf(identifier,"%*s", len, ident); } return identifier; } char config_options[STR_MAX_LEN] = {0}; static void formats() { int i; GDALAllRegister(); printf("Supported Formats:\n"); for (i = 0; i < GDALGetDriverCount(); i++) { GDALDriverH ogr_dr = GDALGetDriver(i); int vector = FALSE; int createable = TRUE; const char* tmpl; #if GDAL_VERSION_MAJOR >= 2 char** papszMD = GDALGetMetadata(ogr_dr, NULL); vector = CSLFetchBoolean(papszMD, GDAL_DCAP_VECTOR, FALSE); createable = CSLFetchBoolean(papszMD, GDAL_DCAP_CREATE, FALSE); #else createable = GDALDatasetTestCapability(ogr_dr, ODrCCreateDataSource); #endif /* Skip raster data sources */ if (! vector) { continue; } /* Report sources w/ create capability as r/w */ if (createable) { tmpl = " -> \"%s\" (read/write)\n"; } else { tmpl = " -> \"%s\" (readonly)\n"; } printf(tmpl, GDALGetDriverShortName(ogr_dr)); } exit(0); } static void usage() { printf( "usage: ogr_fdw_info -s -l -i -t -n -o \n" " ogr_fdw_info -s \n" "usage: ogr_fdw_info -f\n" " Show what input file formats are supported.\n" "\n"); printf( "note (1): You can specify either -l (layer name) or -i (layer index)\n" " if you specify both -l will be used\n" "note (2): config options are specified as a comma deliminated list without the OGR__ prefix\n" " so OGR_XLSX_HEADERS = FORCE OGR_XLSX_FIELD_TYPES = STRING would become:\n" " \"HEADERS = FORCE,FIELD_TYPES = STRING\"" "\n"); exit(0); } int main(int argc, char** argv) { int ch; char* source = NULL; const char* layer = NULL, *server = NULL, *table = NULL, *options = NULL; int layer_index = -1; OGRErr err = OGRERR_NONE; /* If no options are specified, display usage */ if (argc == 1) { usage(); } while ((ch = getopt(argc, argv, "hfs:l:t:n:i:o:")) != -1) { switch (ch) { case 's': source = optarg; break; case 'l': layer = optarg; break; case 'f': formats(); break; case 't': table = optarg; break; case 'n': server = optarg; break; case 'i': layer_index = atoi(optarg) - 1; break; case 'o': options = optarg; break; case '?': case 'h': default: usage(); break; } } if (source && ! layer && layer_index == -1) { err = ogrListLayers(source); } else if (source && (layer || layer_index > -1)) { if (! layer) { err = ogrFindLayer(source, layer_index, &layer); } if (err == OGRERR_NONE) { err = ogrGenerateSQL(server, layer, table, source, options); } } else if (! source && ! layer) { usage(); } if (err != OGRERR_NONE) { printf("OGR Error: %s\n\n", CPLGetLastErrorMsg()); exit(1); } OGRCleanupAll(); exit(0); } static OGRErr ogrListLayers(const char* source) { GDALDatasetH ogr_ds = NULL; int i; GDALAllRegister(); #if GDAL_VERSION_MAJOR < 2 ogr_ds = OGROpen(source, FALSE, NULL); #else ogr_ds = GDALOpenEx(source, GDAL_OF_VECTOR | GDAL_OF_READONLY, NULL, NULL, NULL); #endif if (! ogr_ds) { CPLError(CE_Failure, CPLE_AppDefined, "Could not connect to source '%s'", source); return OGRERR_FAILURE; } printf("Format: %s\n\n", GDALGetDriverShortName(GDALGetDatasetDriver(ogr_ds))); printf("Layers:\n"); for (i = 0; i < GDALDatasetGetLayerCount(ogr_ds); i++) { OGRLayerH ogr_lyr = GDALDatasetGetLayer(ogr_ds, i); if (! ogr_lyr) { return OGRERR_FAILURE; } printf(" %s\n", OGR_L_GetName(ogr_lyr)); } printf("\n"); GDALClose(ogr_ds); return OGRERR_NONE; } static OGRErr ogrGenerateSQL(const char* server, const char* layer, const char* table, const char* source, const char* options) { OGRErr err; GDALDatasetH ogr_ds = NULL; GDALDriverH ogr_dr = NULL; OGRLayerH ogr_lyr = NULL; char server_name[NAMEDATALEN]; stringbuffer_t buf; char **option_iter; char **option_list; GDALAllRegister(); #if GDAL_VERSION_MAJOR < 2 ogr_ds = OGROpen(source, FALSE, &ogr_dr); #else ogr_ds = GDALOpenEx(source, GDAL_OF_VECTOR | GDAL_OF_READONLY, NULL, NULL, NULL); #endif if (! ogr_ds) { CPLError(CE_Failure, CPLE_AppDefined, "Could not connect to source '%s'", source); return OGRERR_FAILURE; } if (! ogr_dr) ogr_dr = GDALGetDatasetDriver(ogr_ds); strcpy(server_name, server == NULL ? "myserver" : server); if (options != NULL) { char *p; char stripped_config_options[STR_MAX_LEN] = {0}; char option[NAMEDATALEN]; const char *short_name = GDALGetDriverShortName(ogr_dr); strncpy(stripped_config_options, options, STR_MAX_LEN - 1); p = strtok(strip_spaces(stripped_config_options), ","); while (p != NULL) { if (strcmp(short_name, "XLSX") == 0 || strcmp(short_name, "XLSX") == 0 || strcmp(short_name, "ODS") == 0) { /* Unify the handling of the options of spreadsheet file options as they are all the same except they have their * Driver Short Name included in the option */ sprintf(option, "OGR_%s_%s ", short_name, ogr_fdw_strupr(p)); } else { sprintf(option, "%s ", ogr_fdw_strupr(p)); } strcat(config_options, option); p = strtok(NULL, ","); } } option_list = CSLTokenizeString(config_options); for ( option_iter = option_list; option_iter && *option_iter; option_iter++ ) { char *key; const char *value; value = CPLParseNameValue(*option_iter, &key); if (! (key && value)) CPLError(CE_Failure, CPLE_AppDefined, "bad config option string '%s'", config_options); CPLSetConfigOption(key, value); CPLFree(key); } CSLDestroy( option_list ); ogr_lyr = GDALDatasetGetLayerByName(ogr_ds, layer); if (! ogr_lyr) { CPLError(CE_Failure, CPLE_AppDefined, "Could not find layer '%s' in source '%s'", layer, source); return OGRERR_FAILURE; } /* Output SERVER definition */ printf("\nCREATE SERVER %s\n" " FOREIGN DATA WRAPPER ogr_fdw\n" " OPTIONS (\n" " datasource '%s',\n" " format '%s'", quote_identifier(server_name), source, GDALGetDriverShortName(ogr_dr)); if (strlen(config_options) > 0) { printf(",\n config_options '%s');\n", config_options); } else { printf(");\n"); } stringbuffer_init(&buf); err = ogrLayerToSQL(ogr_lyr, server_name, TRUE, /* launder table names */ TRUE, /* launder column names */ table,/* output table name */ TRUE, /* use postgis geometry */ &buf); GDALClose(ogr_ds); if (err != OGRERR_NONE) { return err; } printf("\n%s\n", stringbuffer_getstring(&buf)); stringbuffer_release(&buf); return OGRERR_NONE; } static OGRErr ogrFindLayer(const char *source, int layerno, const char** layer) { GDALDatasetH ogr_ds = NULL; int i; char **option_iter; char **option_list; GDALAllRegister(); option_list = CSLTokenizeString(config_options); for (option_iter = option_list; option_iter && *option_iter; option_iter++) { char *key; const char *value; value = CPLParseNameValue(*option_iter, &key); if (! (key && value)) CPLError(CE_Failure, CPLE_AppDefined, "bad config option string '%s'", config_options); CPLSetConfigOption(key, value); CPLFree(key); } CSLDestroy(option_list); #if GDAL_VERSION_MAJOR < 2 ogr_ds = OGROpen(source, FALSE, NULL); #else ogr_ds = GDALOpenEx(source, GDAL_OF_VECTOR | GDAL_OF_READONLY, NULL, NULL, NULL); #endif if (! ogr_ds) { CPLError(CE_Failure, CPLE_AppDefined, "Could not connect to source '%s'", source); return OGRERR_FAILURE; } for (i = 0; i < GDALDatasetGetLayerCount(ogr_ds); i++) { if (i == layerno) { OGRLayerH ogr_lyr = GDALDatasetGetLayer(ogr_ds, i); if (! ogr_lyr) { return OGRERR_FAILURE; } *layer = OGR_L_GetName(ogr_lyr); return OGRERR_NONE; } } GDALClose(ogr_ds); return OGRERR_FAILURE; } static int reserved_word(const char * pgcolumn) { char* reserved[] = { "all", "analyse", "analyze", "and", "any", "array", "as", "asc", "asymmetric", "authorization", "binary", "both", "case", "cast", "check", "collate", "collation", "column", "concurrently", "constraint", "create", "cross", "current_catalog", "current_date", "current_role", "current_schema", "current_time", "current_timestamp", "current_user", "default", "deferrable", "desc", "distinct", "do", "else", "end", "except", "false", "fetch", "for", "foreign", "freeze", "from", "full", "grant", "group", "having", "ilike", "in", "initially", "inner", "intersect", "into", "is", "isnull", "join", "lateral", "leading", "left", "like", "limit", "localtime", "localtimestamp", "natural", "not", "notnull", "null", "offset", "on", "only", "or", "order", "outer", "overlaps", "placing", "primary", "references", "returning", "right", "select", "session_user", "similar", "some", "symmetric", "table", "tablesample", "then", "to", "trailing", "true", "union", "unique", "user", "using", "variadic", "verbose", "when", "where", "window", "with" }; int i; for (i = 0; i < sizeof(reserved)/sizeof(reserved[0]); i++) { if (strcmp(pgcolumn, reserved[i]) == 0) return 1; } return 0; } pgsql-ogr-fdw-1.1.5/output/000077500000000000000000000000001463733357200155645ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/output/file.source000066400000000000000000000121211463733357200177220ustar00rootroot00000000000000CREATE EXTENSION ogr_fdw; CREATE SERVER myserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile' ); ------------------------------------------------ CREATE FOREIGN TABLE pt_1 ( fid integer, geom bytea, name varchar, age integer, height double precision, birthdate date ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_1 WHERE fid = 1; fid | geom | name | age | height | birthdate -----+----------------------------------------------+------+-----+--------+------------ 1 | \x010100000054e943acd697e2bfc0895ee54a46cf3f | Paul | 33 | 5.84 | 03-25-1971 (1 row) ------------------------------------------------ CREATE FOREIGN TABLE pt_2 ( fid integer, name varchar ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_2 ORDER BY name; fid | name -----+------- 1 | Paul 0 | Peter (2 rows) ------------------------------------------------ CREATE FOREIGN TABLE pt_3 ( geom bytea, name varchar ) SERVER myserver OPTIONS ( layer 'pt_two' ); SELECT * FROM pt_3 ORDER BY name; geom | name ----------------------------------------------+------- \x010100000054e943acd697e2bfc0895ee54a46cf3f | Paul \x0101000000c00497d1162cb93f8cbaef08a080e63f | Peter (2 rows) ------------------------------------------------ CREATE FOREIGN TABLE poly_1 ( fid bigint, geom bytea, id double precision, name varchar(5) ) SERVER myserver OPTIONS (layer 'poly'); SELECT length(geom) FROM poly_1 WHERE name = 'Three'; length -------- 307 (1 row) ------------------------------------------------ -- Laundering and explicit column naming test CREATE FOREIGN TABLE column_name_test ( fid integer, name varchar OPTIONS (column_name '2ame'), theage integer OPTIONS (column_name 'age'), height real OPTIONS (column_name 'Height'), birthdate date OPTIONS (column_name 'b-rthdate') ) SERVER myserver OPTIONS (layer '2launder'); SELECT * FROM column_name_test ORDER BY fid; fid | name | theage | height | birthdate -----+-------+--------+--------+------------ 0 | Peter | 45 | 5.6 | 04-12-1965 1 | Paul | 33 | 5.84 | 03-25-1971 (2 rows) -- Check that columns are reverse-laundered when generating -- OGR SQL filters SET client_min_messages = debug1; SELECT name FROM column_name_test WHERE name = 'Paul'; DEBUG: OGR SQL: ("2ame" = 'Paul') name ------ Paul (1 row) SET client_min_messages = notice; ------------------------------------------------ -- GDAL options passing tests CREATE SERVER myserver_latin1 FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile', config_options 'SHAPE_ENCODING=LATIN1' ); CREATE FOREIGN TABLE e_1 ( fid integer, name varchar ) SERVER myserver_latin1 OPTIONS ( layer 'enc' ); SET client_min_messages = debug1; SELECT fid, name FROM e_1 WHERE fid = 1; DEBUG: GDAL config option 'SHAPE_ENCODING' set to 'LATIN1' DEBUG: OGR SQL: (fid = 1) DEBUG: GDAL config option 'SHAPE_ENCODING' set to 'LATIN1' fid | name -----+------ 1 | Pàul (1 row) SET client_min_messages = notice; ------------------------------------------------ -- Using encoding option directly CREATE SERVER myserver_latin1_direct FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data', format 'ESRI Shapefile', character_encoding 'LATIN1' ); CREATE FOREIGN TABLE e_2 ( fid integer, name varchar ) SERVER myserver_latin1_direct OPTIONS ( layer 'enc' ); SELECT fid, name FROM e_2 WHERE fid = 1; fid | name -----+------ 1 | Pàul (1 row) ------------------------------------------------ -- Geometryless test CREATE SERVER csvserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data/no_geom.csv', format 'CSV' ); CREATE FOREIGN TABLE no_geom ( fid bigint, name varchar, age varchar, value varchar ) SERVER csvserver OPTIONS (layer 'no_geom'); SELECT c.* FROM generate_series(1,4) g JOIN no_geom c ON (c.fid = g.g); fid | name | age | value -----+---------+-----+------- 1 | Peter | 34 | 10.2 2 | John | 77 | 3.4 3 | Paul | 45 | 19.2 4 | Matthew | 35 | 18.2 (4 rows) ------------------------------------------------ -- FGDB test CREATE SERVER fgdbserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '/vsizip/@abs_srcdir@/data/Querying.zip/Querying.gdb', format 'OpenFileGDB' ); CREATE FOREIGN TABLE cities ( fid bigint, shape bytea, city_fips varchar(5), city_name varchar(40), state_fips varchar(2), state_name varchar(25), state_city varchar(7), type varchar(25), capital varchar(1), elevation integer, pop1990 integer, popcat integer ) SERVER "fgdbserver" OPTIONS (layer 'Cities'); SET client_min_messages = LOG; SELECT fid, city_name, pop1990 FROM cities WHERE pop1990 = 17710; fid | city_name | pop1990 -----+--------------+--------- 9 | Port Angeles | 17710 (1 row) SELECT fid, city_name, pop1990 FROM cities WHERE city_name = 'Williston'; fid | city_name | pop1990 -----+-----------+--------- 8 | Williston | 13131 (1 row) pgsql-ogr-fdw-1.1.5/output/import.source000066400000000000000000000067321463733357200203300ustar00rootroot00000000000000set client_min_messages=NOTICE; ------------------------------------------------ CREATE SCHEMA imp1; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO (n2launder) FROM SERVER myserver INTO imp1; NOTICE: Number of tables to be created 1 SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp1' and fc.relname = 'n2launder' ORDER BY c.ordinal_position; column_name | data_type | attfdwoptions -------------+-------------------+------------------------- fid | bigint | geom | bytea | n2ame | character varying | {column_name=2ame} age | integer | height | double precision | b_rthdate | date | {column_name=b-rthdate} (6 rows) SELECT * FROM imp1.n2launder WHERE fid = 0; fid | geom | n2ame | age | height | b_rthdate -----+----------------------------------------------+-------+-----+--------+------------ 0 | \x0101000000c00497d1162cb93f8cbaef08a080e63f | Peter | 45 | 5.6 | 04-12-1965 (1 row) ------------------------------------------------ CREATE SCHEMA imp2; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO ("natural") FROM SERVER myserver INTO imp2; NOTICE: Number of tables to be created 1 SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp2' and fc.relname = 'natural' ORDER BY c.ordinal_position; column_name | data_type | attfdwoptions -------------+-------------------+--------------- fid | bigint | id | double precision | natural | character varying | (3 rows) SELECT "natural" FROM imp2."natural"; natural --------- wood land (2 rows) ------------------------------------------------ CREATE SERVER svr_test_apost FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource '@abs_srcdir@/data/no_geom_apost.csv', format 'CSV' ); CREATE SCHEMA imp3; IMPORT FOREIGN SCHEMA ogr_all LIMIT TO (no_geom_apost) FROM SERVER svr_test_apost INTO imp3; NOTICE: Number of tables to be created 1 SELECT c.column_name, c.data_type, attfdwoptions FROM information_schema._pg_foreign_table_columns AS fc INNER JOIN information_schema.columns AS c ON fc.nspname = c.table_schema AND fc.relname = c.table_name AND fc.attname = c.column_name WHERE fc.nspname = 'imp3' and fc.relname = 'no_geom_apost' ORDER BY c.ordinal_position; column_name | data_type | attfdwoptions ----------------+-------------------+-------------------------------- fid | bigint | name | character varying | age | character varying | person_s_value | character varying | {"column_name=person's value"} (4 rows) SELECT name,age FROM imp3.no_geom_apost WHERE name = 'Paul'; name | age ------+----- Paul | 45 (1 row) SELECT name,age FROM imp3.no_geom_apost WHERE name IS NULL; name | age ------+----- (0 rows) SELECT name,age FROM imp3.no_geom_apost WHERE name = ''; name | age ------+----- | 44 (1 row) ------------------------------------------------ pgsql-ogr-fdw-1.1.5/output/pgsql.source000066400000000000000000000165271463733357200201470ustar00rootroot00000000000000---------------------------------------------------------------------- -- Create local table CREATE TABLE bytea_local ( fid serial primary key, geom bytea, name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ); ---------------------------------------------------------------------- -- Populate local table INSERT INTO bytea_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Jim', '14232'::bytea, 23, 1, 4.3, 5.5, '2010-10-10'::date, '13:23:21'::time, '2010-10-10 13:23:21'::timestamp, 'this', 'y' ), ('Marvin', '55555'::bytea, 34, 2, 5.4, 10.13, '2011-11-11'::date, '15:21:45'::time, '2011-11-11 15:21:45'::timestamp, 'that', 'n' ), (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote table CREATE SERVER pgserver FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'PG:dbname=contrib_regression host=localhost', format 'PostgreSQL' ); CREATE FOREIGN TABLE bytea_fdw ( fid bigint, geom bytea, name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ) SERVER pgserver OPTIONS (layer 'bytea_local'); SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw; fid | name | geom | age | size | value | num | dt | tm | dttm | varch | yn -----+--------+--------------+-----+------+-------+-------+------------+----------+--------------------------+----------+---- 1 | Jim | \x3134323332 | 23 | 1 | 4.3 | 5.50 | 10-10-2010 | 13:23:21 | Sun Oct 10 13:23:21 2010 | this | y 2 | Marvin | \x3535353535 | 34 | 2 | 5.4 | 10.13 | 11-11-2011 | 15:21:45 | Fri Nov 11 15:21:45 2011 | that | n 3 | | | | | | | | | | | (3 rows) SELECT a.name, b.name FROM bytea_local a JOIN bytea_fdw b USING (fid); name | name --------+-------- Jim | Jim Marvin | Marvin | (3 rows) EXPLAIN VERBOSE SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw; QUERY PLAN ----------------------------------------------------------------------------- Foreign Scan on public.bytea_fdw (cost=25.00..1025.00 rows=1000 width=170) Output: fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn (2 rows) ---------------------------------------------------------------------- -- Remote Query and OGR SQL pushdown SET client_min_messages = DEBUG1; SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM bytea_fdw WHERE fid = 4; DEBUG: OGR SQL: (fid = 4) fid | name | geom | age | size | value | num | dt | tm | dttm | varch | yn -----+------+------+-----+------+-------+-----+----+----+------+-------+---- (0 rows) SELECT fid, name, dt FROM bytea_fdw WHERE name IS NULL; DEBUG: OGR SQL: (name IS NULL) fid | name | dt -----+------+---- 3 | | (1 row) SELECT fid, name FROM bytea_fdw WHERE name = 'Jim' AND age <= 30; DEBUG: OGR SQL: (age <= 30) AND (name = 'Jim') fid | name -----+------ 1 | Jim (1 row) SELECT fid, name, dt FROM bytea_fdw WHERE name = 'Jim' AND age <= 30 AND dt > '2010-10-1'::date; DEBUG: OGR SQL: (age <= 30) AND (dt > '10-01-2010') AND (name = 'Jim') fid | name | dt -----+------+------------ 1 | Jim | 10-10-2010 (1 row) SELECT fid, name FROM bytea_fdw WHERE name = 'Jim' OR name IS NULL; DEBUG: OGR SQL: ((name = 'Jim') OR (name IS NULL)) fid | name -----+------ 1 | Jim 3 | (2 rows) ---------------------------------------------------------------------- -- Cached query case, exercised by statement handles or -- functions. CREATE OR REPLACE FUNCTION get_names() RETURNS varchar AS $$ BEGIN RETURN (SELECT string_agg(name,',') FROM bytea_fdw WHERE name = 'Jim' OR name IS NULL); END; $$ LANGUAGE 'plpgsql'; SELECT get_names(); DEBUG: OGR SQL: ((name = 'Jim') OR (name IS NULL)) get_names ----------- Jim (1 row) DROP FUNCTION get_names(); ---------------------------------------------------------------------- -- Remote Update INSERT INTO bytea_fdw (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Margaret', '2222'::bytea, 12, 5, 1.4, 19.13, '2001-11-23'::date, '9:12:34'::time, '2001-02-11 09:23:11'::timestamp, 'them', 'y' ) RETURNING fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn; fid | name | geom | age | size | value | num | dt | tm | dttm | varch | yn -----+----------+------------+-----+------+-------+-------+------------+----------+--------------------------+----------+---- 4 | Margaret | \x32323232 | 12 | 5 | 1.4 | 19.13 | 11-23-2001 | 09:12:34 | Sun Feb 11 09:23:11 2001 | them | y (1 row) UPDATE bytea_fdw SET name = 'Maggie', num = 45.34, yn = 'n' WHERE age = 12; DEBUG: OGR SQL: (age = 12) SELECT fid, name, num, yn FROM bytea_fdw WHERE fid = 4; DEBUG: OGR SQL: (fid = 4) fid | name | num | yn -----+--------+-------+---- 4 | Maggie | 45.34 | n (1 row) UPDATE bytea_fdw SET dt = '2089-12-13', tm = '01:23:45' WHERE num = 45.34; DEBUG: OGR SQL: (num = 45.34) SELECT fid, dt, tm FROM bytea_fdw WHERE fid = 4; DEBUG: OGR SQL: (fid = 4) fid | dt | tm -----+------------+---------- 4 | 12-13-2089 | 01:23:45 (1 row) DELETE FROM bytea_fdw WHERE fid = 4; DEBUG: OGR SQL: (fid = 4) SELECT a.fid, a.name, b.name FROM bytea_local a JOIN bytea_fdw b USING (fid); fid | name | name -----+--------+-------- 1 | Jim | Jim 2 | Marvin | Marvin 3 | | (3 rows) ---------------------------------------------------------------------- -- Populate local array table SET client_min_messages = NOTICE; CREATE TABLE array_local ( fid integer primary key, geom bytea, txt text[], int int2[], flt float4[], b boolean[] ); INSERT INTO array_local (fid,txt, int, flt, b) VALUES (1, ARRAY['Jim'], ARRAY[1,2,3], ARRAY[3.4,5.6,7.8], ARRAY[true,false]), (2, ARRAY['Jim',NULL,'Joe'], ARRAY[1,3,NULL,4], ARRAY[4.5,NULL,3.4], ARRAY[false,NULL]), (3, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote array table CREATE FOREIGN TABLE array_fdw ( fid bigint, geom bytea, txt text[], int int4[], flt float8[], b boolean[] ) SERVER pgserver OPTIONS (layer 'array_local'); SELECT fid, txt, int, flt, b FROM array_fdw; fid | txt | int | flt | b -----+--------------+-----------+---------------+------- 1 | {Jim} | {1,2,3} | {3.4,5.6,7.8} | {t,f} 2 | {Jim,"",Joe} | {1,3,0,4} | {4.5,0,3.4} | {f,f} 3 | | | | (3 rows) ---------------------------------------------------------------------- -- Update remote array table UPDATE array_fdw SET txt = ARRAY['newJim', 'newJoe'], int = ARRAY[-2, -1, 0, 1, 2], flt = ARRAY[-0.1, 0.0, 0.1] WHERE fid = 3; SELECT txt, int, flt FROM array_fdw WHERE fid = 3; txt | int | flt -----------------+---------------+-------------- {newJim,newJoe} | {-2,-1,0,1,2} | {-0.1,0,0.1} (1 row) pgsql-ogr-fdw-1.1.5/output/postgis.source000066400000000000000000000106471463733357200205060ustar00rootroot00000000000000CREATE EXTENSION postgis; CREATE TABLE geometry_local ( fid serial primary key, geom geometry(Point, 4326), name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ); ---------------------------------------------------------------------- -- Populate local table INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Jim', 'SRID=4326;POINT(0 0)', 23, 1, 4.3, 5.5, '2010-10-10'::date, '13:23:21'::time, '2010-10-10 13:23:21'::timestamp, 'this', 'y' ); INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES ('Marvin', 'SRID=4326;POINT(100 0)', 34, 2, 5.4, 10.13, '2011-11-11'::date, '15:21:45'::time, '2011-11-11 15:21:45'::timestamp, 'that', 'n' ); INSERT INTO geometry_local (name, geom, age, size, value, num, dt, tm, dttm, varch, yn) VALUES (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); ---------------------------------------------------------------------- -- Create remote table CREATE SERVER pgservergeom FOREIGN DATA WRAPPER ogr_fdw OPTIONS ( datasource 'PG:dbname=contrib_regression host=localhost', format 'PostgreSQL' ); CREATE FOREIGN TABLE geometry_fdw ( fid integer, geom geometry(point, 4326), name varchar, age bigint, size integer, value float8, num numeric(6,2), dt date, tm time, dttm timestamp, varch char(8), yn char ) SERVER pgservergeom OPTIONS (layer 'geometry_local'); SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM geometry_fdw; fid | name | geom | age | size | value | num | dt | tm | dttm | varch | yn -----+--------+----------------------------------------------------+-----+------+-------+-------+------------+----------+--------------------------+----------+---- 1 | Jim | 0101000020E610000000000000000000000000000000000000 | 23 | 1 | 4.3 | 5.50 | 10-10-2010 | 13:23:21 | Sun Oct 10 13:23:21 2010 | this | y 2 | Marvin | 0101000020E610000000000000000059400000000000000000 | 34 | 2 | 5.4 | 10.13 | 11-11-2011 | 15:21:45 | Fri Nov 11 15:21:45 2011 | that | n 3 | | | | | | | | | | | (3 rows) SELECT a.name, b.name FROM geometry_local a JOIN geometry_fdw b USING (fid); name | name --------+-------- Jim | Jim Marvin | Marvin | (3 rows) EXPLAIN VERBOSE SELECT fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn FROM geometry_fdw; QUERY PLAN -------------------------------------------------------------------------------- Foreign Scan on public.geometry_fdw (cost=25.00..1025.00 rows=1000 width=166) Output: fid, name, geom, age, size, value, num, dt, tm, dttm, varch, yn (2 rows) ---------------------------------------------------------------------- -- Remote Query and OGR SQL pushdown SET client_min_messages = DEBUG1; SELECT name, age, ST_AsText(geom) FROM geometry_fdw WHERE name = 'Jim' AND age <= 30 AND geom && ST_MakeEnvelope(-1, -1, 1, 1, 4326); DEBUG: OGR SQL: (age <= 30) AND (name = 'Jim') DEBUG: OGR spatial filter (-1 -1, 1 1) name | age | st_astext ------+-----+------------ Jim | 23 | POINT(0 0) (1 row) SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_Intersects(geom, ST_MakeEnvelope(-1, -1, 1, 1, 4326)); DEBUG: OGR spatial filter (-1 -1, 1 1) name | st_astext ------+------------ Jim | POINT(0 0) (1 row) SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE geom && ST_MakeEnvelope(-180, -90, 180, 90, 4326); DEBUG: OGR spatial filter (-180 -90, 180 90) name | st_astext --------+-------------- Jim | POINT(0 0) Marvin | POINT(100 0) (2 rows) SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_MakeEnvelope(-180, -90, 180, 90, 4326) && geom; DEBUG: OGR spatial filter (-180 -90, 180 90) name | st_astext --------+-------------- Jim | POINT(0 0) Marvin | POINT(100 0) (2 rows) SELECT name, ST_AsText(geom) FROM geometry_fdw WHERE ST_MakeEnvelope(-180, -90, 180, 90, 4326) && ST_MakeEnvelope(-180, -90, 180, 90, 4326); name | st_astext --------+-------------- Jim | POINT(0 0) Marvin | POINT(100 0) | (3 rows) pgsql-ogr-fdw-1.1.5/sql/000077500000000000000000000000001463733357200150235ustar00rootroot00000000000000pgsql-ogr-fdw-1.1.5/sql/.gitignore000066400000000000000000000001071463733357200170110ustar00rootroot00000000000000# Ignore everything in this directory * # Except this file !.gitignore pgsql-ogr-fdw-1.1.5/stringbuffer.c000066400000000000000000000173421463733357200170770ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * stringbuffer.c * simple stringbuffer * * Copyright (c) 2009, Paul Ramsey * Copyright (c) 2002 Thamer Alharbash * *------------------------------------------------------------------------- */ #include "stringbuffer.h" #pragma GCC diagnostic push #ifndef __clang__ #pragma GCC diagnostic ignored "-Wsuggest-attribute=format" #endif /** * Allocate a new stringbuffer_t. Use stringbuffer_destroy to free. */ stringbuffer_t* stringbuffer_create(void) { return stringbuffer_create_with_size(STRINGBUFFER_STARTSIZE); } static void stringbuffer_init_with_size(stringbuffer_t* s, size_t size) { s->str_start = malloc(size); s->str_end = s->str_start; s->capacity = size; memset(s->str_start, 0, size); } void stringbuffer_release(stringbuffer_t* s) { if (s->str_start) { free(s->str_start); } } void stringbuffer_init(stringbuffer_t* s) { stringbuffer_init_with_size(s, STRINGBUFFER_STARTSIZE); } /** * Allocate a new stringbuffer_t. Use stringbuffer_destroy to free. */ stringbuffer_t* stringbuffer_create_with_size(size_t size) { stringbuffer_t* s; s = malloc(sizeof(stringbuffer_t)); stringbuffer_init_with_size(s, size); return s; } /** * Free the stringbuffer_t and all memory managed within it. */ void stringbuffer_destroy(stringbuffer_t* s) { stringbuffer_release(s); if (s) { free(s); } } /** * Reset the stringbuffer_t. Useful for starting a fresh string * without the expense of freeing and re-allocating a new * stringbuffer_t. */ void stringbuffer_clear(stringbuffer_t* s) { s->str_start[0] = '\0'; s->str_end = s->str_start; } /** * If necessary, expand the stringbuffer_t internal buffer to accomodate the * specified additional size. */ static inline void stringbuffer_makeroom(stringbuffer_t* s, size_t size_to_add) { size_t current_size = (s->str_end - s->str_start); size_t capacity = s->capacity; size_t required_size = current_size + size_to_add; while (capacity < required_size) { capacity *= 2; } if (capacity > s->capacity) { s->str_start = realloc(s->str_start, capacity); s->capacity = capacity; s->str_end = s->str_start + current_size; } } /** * Return the last character in the buffer. */ char stringbuffer_lastchar(stringbuffer_t* s) { if (s->str_end == s->str_start) { return 0; } return * (s->str_end - 1); } /** * Append the specified string to the stringbuffer_t. */ void stringbuffer_append(stringbuffer_t* s, const char* a) { int alen = strlen(a); /* Length of string to append */ int alen0 = alen + 1; /* Length including null terminator */ stringbuffer_makeroom(s, alen0); memcpy(s->str_end, a, alen0); s->str_end += alen; } /** * Append the specified character to the stringbuffer_t. */ void stringbuffer_append_char(stringbuffer_t* s, char c) { stringbuffer_makeroom(s, 2); /* space for char + null terminator */ * (s->str_end) = c; /* add char */ s->str_end += 1; * (s->str_end) = 0; /* null terminate */ } /** * Returns a reference to the internal string being managed by * the stringbuffer. The current string will be null-terminated * within the internal string. */ const char* stringbuffer_getstring(stringbuffer_t* s) { return s->str_start; } /** * Returns a newly allocated string large enough to contain the * current state of the string. Caller is responsible for * freeing the return value. */ char* stringbuffer_getstringcopy(stringbuffer_t* s) { size_t size = (s->str_end - s->str_start) + 1; char* str = malloc(size); memcpy(str, s->str_start, size); str[size - 1] = '\0'; return str; } /** * Returns the length of the current string, not including the * null terminator (same behavior as strlen()). */ int stringbuffer_getlength(stringbuffer_t* s) { return (s->str_end - s->str_start); } /** * Clear the stringbuffer_t and re-start it with the specified string. */ void stringbuffer_set(stringbuffer_t* s, const char* str) { stringbuffer_clear(s); stringbuffer_append(s, str); } /** * Copy the contents of src into dst. */ void stringbuffer_copy(stringbuffer_t* dst, stringbuffer_t* src) { stringbuffer_set(dst, stringbuffer_getstring(src)); } /** * Appends a formatted string to the current string buffer, * using the format and argument list provided. Returns -1 on error, * check errno for reasons, documented in the printf man page. */ static int stringbuffer_avprintf(stringbuffer_t* s, const char* fmt, va_list ap) { int maxlen = (s->capacity - (s->str_end - s->str_start)); int len = 0; /* Length of the output */ va_list ap2; /* Make a copy of the variadic arguments, in case we need to print twice */ /* Print to our buffer */ va_copy(ap2, ap); len = vsnprintf(s->str_end, maxlen, fmt, ap2); va_end(ap2); /* Propogate errors up */ if (len < 0) #if defined(__MINGW64_VERSION_MAJOR) len = _vscprintf(fmt, ap2); /**Assume windows flaky vsnprintf that returns -1 if initial buffer to small and add more space **/ #else return len; #endif /* We didn't have enough space! */ /* Either Unix vsnprint returned write length larger than our buffer */ /* or Windows vsnprintf returned an error code. */ if (len >= maxlen) { stringbuffer_makeroom(s, len + 1); maxlen = (s->capacity - (s->str_end - s->str_start)); /* Try to print a second time */ len = vsnprintf(s->str_end, maxlen, fmt, ap); /* Printing error? Error! */ if (len < 0) { return len; } /* Too long still? Error! */ if (len >= maxlen) { return -1; } } /* Move end pointer forward and return. */ s->str_end += len; return len; } /** * Appends a formatted string to the current string buffer, * using the format and argument list provided. * Returns -1 on error, check errno for reasons, * as documented in the printf man page. */ int stringbuffer_aprintf(stringbuffer_t* s, const char* fmt, ...) { int r; va_list ap; va_start(ap, fmt); r = stringbuffer_avprintf(s, fmt, ap); va_end(ap); return r; } /** * Trims whitespace off the end of the stringbuffer. Returns * the number of characters trimmed. */ int stringbuffer_trim_trailing_white(stringbuffer_t* s) { char* ptr = s->str_end; int dist = 0; /* Roll backwards until we hit a non-space. */ while (ptr > s->str_start) { ptr--; if ((*ptr == ' ') || (*ptr == '\t')) { continue; } else { ptr++; dist = s->str_end - ptr; *ptr = '\0'; s->str_end = ptr; return dist; } } return dist; } /** * Trims zeroes off the end of the last number in the stringbuffer. * The number has to be the very last thing in the buffer. Only the * last number will be trimmed. Returns the number of characters * trimmed. * * eg: 1.22000 -> 1.22 * 1.0 -> 1 * 0.0 -> 0 */ int stringbuffer_trim_trailing_zeroes(stringbuffer_t* s) { char* ptr = s->str_end; char* decimal_ptr = NULL; int dist; if (s->str_end - s->str_start < 2) { return 0; } /* Roll backwards to find the decimal for this number */ while (ptr > s->str_start) { ptr--; if (*ptr == '.') { decimal_ptr = ptr; break; } if ((*ptr >= '0') && (*ptr <= '9')) { continue; } else { break; } } /* No decimal? Nothing to trim! */ if (! decimal_ptr) { return 0; } ptr = s->str_end; /* Roll backwards again, with the decimal as stop point, trimming contiguous zeroes */ while (ptr >= decimal_ptr) { ptr--; if (*ptr == '0') { continue; } else { break; } } /* Huh, we get anywhere. Must not have trimmed anything. */ if (ptr == s->str_end) { return 0; } /* If we stopped at the decimal, we want to null that out. It we stopped on a numeral, we want to preserve that, so push the pointer forward one space. */ if (*ptr != '.') { ptr++; } /* Add null terminator re-set the end of the stringbuffer. */ *ptr = '\0'; dist = s->str_end - ptr; s->str_end = ptr; return dist; } pgsql-ogr-fdw-1.1.5/stringbuffer.h000066400000000000000000000031201463733357200170710ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * stringbuffer.h * simple stringbuffer * * Copyright (c) 2009, Paul Ramsey * Copyright (c) 2002 Thamer Alharbash * *------------------------------------------------------------------------- */ #ifndef _STRINGBUFFER_H #define _STRINGBUFFER_H 1 #include #include #include #include #define STRINGBUFFER_STARTSIZE 128 typedef struct { size_t capacity; char *str_end; char *str_start; } stringbuffer_t; extern stringbuffer_t *stringbuffer_create_with_size(size_t size); extern stringbuffer_t *stringbuffer_create(void); extern void stringbuffer_init(stringbuffer_t *s); extern void stringbuffer_release(stringbuffer_t *s); extern void stringbuffer_destroy(stringbuffer_t *sb); extern void stringbuffer_clear(stringbuffer_t *sb); void stringbuffer_set(stringbuffer_t *sb, const char *s); void stringbuffer_copy(stringbuffer_t *sb, stringbuffer_t *src); extern void stringbuffer_append(stringbuffer_t *sb, const char *s); extern void stringbuffer_append_char(stringbuffer_t *s, char c); extern int stringbuffer_aprintf(stringbuffer_t *sb, const char *fmt, ...); extern const char *stringbuffer_getstring(stringbuffer_t *sb); extern char *stringbuffer_getstringcopy(stringbuffer_t *sb); extern int stringbuffer_getlength(stringbuffer_t *sb); extern char stringbuffer_lastchar(stringbuffer_t *s); extern int stringbuffer_trim_trailing_white(stringbuffer_t *s); extern int stringbuffer_trim_trailing_zeroes(stringbuffer_t *s); #endif /* _STRINGBUFFER_H */ pgsql-ogr-fdw-1.1.5/stringbuffer_pg.c000066400000000000000000000016431463733357200175620ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * stringbuffer.c * simple stringbuffer * * Copyright (c) 2009, Paul Ramsey * Copyright (c) 2002 Thamer Alharbash * *------------------------------------------------------------------------- */ /* * We need a version of stringbuffer that uses palloc/pfree/repalloc * for use inside PgSQL. (We are sharing code in ogr_fdw_common.c * between the commandline utility and the backend module, so we * cannot just depend on the PgSQL standard string handling utility) * We rebuild it here, but with the pgsql memory stuff in place * of the standard system calls */ #include "stringbuffer.h" void * palloc(size_t sz); void pfree(void *ptr); void * repalloc(void *ptr, size_t sz); #define malloc(sz) palloc(sz) #define free(ptr) pfree(ptr) #define realloc(ptr,sz) repalloc(ptr,sz) #include "stringbuffer.c"