diff --git a/Makefile b/Makefile index 60ef3e8..ae77d9c 100755 --- a/Makefile +++ b/Makefile @@ -123,6 +123,7 @@ TESTS = test-$(PYTHON_TEST_VERSION)/sql/multicorn_cache_invalidation.sql test-$(PYTHON_TEST_VERSION)/sql/multicorn_sequence_test.sql \ test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_date.sql \ test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_dict.sql \ + test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_limit.sql \ test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_list.sql \ test-$(PYTHON_TEST_VERSION)/sql/multicorn_test_sort.sql \ test-$(PYTHON_TEST_VERSION)/sql/write_savepoints.sql \ diff --git a/flake.nix b/flake.nix index 606dbc3..86fa1eb 100644 --- a/flake.nix +++ b/flake.nix @@ -156,7 +156,7 @@ pythonVersion = pkgs.lib.versions.majorMinor test_python.version; isPython312OrHigher = pkgs.lib.versionAtLeast pythonVersion "3.12"; - baseTestCount = if pkgs.lib.versionOlder pgMajorVersion "14" then 18 else 19; + baseTestCount = if pkgs.lib.versionOlder pgMajorVersion "14" then 19 else 20; expectedTestCount = toString (baseTestCount - (if isPython312OrHigher then 1 else 0)); in pkgs.stdenv.mkDerivation { name = "multicorn2-python-test-pg${test_postgresql.version}-py${test_python.version}"; diff --git a/python/multicorn/__init__.py b/python/multicorn/__init__.py index bd30c00..9908180 100755 --- a/python/multicorn/__init__.py +++ b/python/multicorn/__init__.py @@ -212,6 +212,25 @@ def can_sort(self, sortkeys): """ return [] + def can_limit(self, limit, offset): + """ + Method called from the planner to ask the FDW whether it supports LIMIT and OFFSET pushdown. + + This method is only called if the entire query can be pushed down. For example, if the query has + a GROUP BY clause, this method will not be called. Or, if only part of the sort is pushed down, + this method will not be called and limit/offset will not be pushed down. + + Currently, we do not support pushing down limit/offset if the query includes a WHERE clause (quals). + + Args: + limit (int or None): The limit to apply to the query, if any. + offset (int or None): The offset to apply to the query, if any. + + Return: + True if the FDW can support both LIMIT and OFFSET pushdown, False otherwise. + """ + return False + def get_path_keys(self): u""" Method called from the planner to add additional Path to the planner. @@ -269,7 +288,7 @@ def get_path_keys(self): """ return [] - def explain(self, quals, columns, sortkeys=None, verbose=False): + def explain(self, quals, columns, sortkeys=None, verbose=False, limit=None, offset=None): """Hook called on explain. The arguments are the same as the :meth:`execute`, with the addition of @@ -280,7 +299,7 @@ def explain(self, quals, columns, sortkeys=None, verbose=False): """ return [] - def execute(self, quals, columns, sortkeys=None): + def execute(self, quals, columns, sortkeys=None, limit=None, offset=None): """Execute a query in the foreign data wrapper. This method is called at the first iteration. @@ -313,6 +332,8 @@ def execute(self, quals, columns, sortkeys=None): should be in the sequence. sortkeys (list): A list of :class:`SortKey` that the FDW said it can enforce. + limit (int or None): The limit to apply to the query, if any. + offset (int or None): The offset to apply to the query, if any. Returns: An iterable of python objects which can be converted back to PostgreSQL. diff --git a/python/multicorn/testfdw.py b/python/multicorn/testfdw.py index cf8c6b6..598eed6 100644 --- a/python/multicorn/testfdw.py +++ b/python/multicorn/testfdw.py @@ -5,7 +5,7 @@ from itertools import cycle from datetime import datetime from operator import itemgetter - +from itertools import islice class TestForeignDataWrapper(ForeignDataWrapper): @@ -15,6 +15,12 @@ def __init__(self, options, columns): super(TestForeignDataWrapper, self).__init__(options, columns) self.columns = columns self.test_type = options.get('test_type', None) + self.canlimit = options.get('canlimit', False) + if isinstance(self.canlimit, str): + self.canlimit = self.canlimit.lower() == 'true' + self.cansort = options.get('cansort', True) + if isinstance(self.cansort, str): + self.cansort = self.cansort.lower() == 'true' self.test_subtype = options.get('test_subtype', None) self.tx_hook = options.get('tx_hook', False) self._modify_batch_size = int(options.get('modify_batch_size', 1)) @@ -79,7 +85,7 @@ def _as_generator(self, quals, columns): index) yield line - def execute(self, quals, columns, sortkeys=None): + def execute(self, quals, columns, sortkeys=None, limit=None, offset=None): sortkeys = sortkeys or [] log_to_postgres(str(sorted(quals))) log_to_postgres(str(sorted(columns))) @@ -99,14 +105,15 @@ def execute(self, quals, columns, sortkeys=None): k = sortkeys[0]; res = self._as_generator(quals, columns) if (self.test_type == 'sequence'): - return sorted(res, key=itemgetter(k.attnum - 1), + res = sorted(res, key=itemgetter(k.attnum - 1), reverse=k.is_reversed) else: - return sorted(res, key=itemgetter(k.attname), + res = sorted(res, key=itemgetter(k.attname), reverse=k.is_reversed) - return self._as_generator(quals, columns) + return res[offset:offset + limit] if offset else res[:limit] + return islice(self._as_generator(quals, columns), offset, (offset or 0) + limit if limit else None) - def explain(self, quals, columns, sortkeys=None, verbose=False): + def explain(self, quals, columns, sortkeys=None, verbose=False, limit=None, offset=None): if self.noisy_explain: log_to_postgres("EXPLAIN quals=%r" % (sorted(quals),)) log_to_postgres("EXPLAIN columns=%r" % (sorted(columns),)) @@ -126,8 +133,13 @@ def get_path_keys(self): return [] def can_sort(self, sortkeys): - # assume sort pushdown ok for all cols, in any order, any collation - return sortkeys + # assume sort pushdown ok only for first sort key + if not self.cansort: + return [] + return sortkeys[:1] + + def can_limit(self, limit, offset): + return self.canlimit def update(self, rowid, newvalues): if self.test_type == 'nowrite': diff --git a/src/multicorn.c b/src/multicorn.c index 2d9c8d0..5ab1ab8 100644 --- a/src/multicorn.c +++ b/src/multicorn.c @@ -56,6 +56,12 @@ static void multicornGetForeignRelSize(PlannerInfo *root, static void multicornGetForeignPaths(PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid); +static void multicornGetForeignUpperPaths(PlannerInfo *root, + UpperRelationKind stage, + RelOptInfo *input_rel, + RelOptInfo *output_rel, + void *extra); + static ForeignScan *multicornGetForeignPlan(PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid, @@ -119,6 +125,15 @@ static void multicorn_xact_callback(XactEvent event, void *arg); void *serializePlanState(MulticornPlanState * planstate); MulticornExecState *initializeExecState(void *internal_plan_state); +static void add_foreign_ordered_paths(PlannerInfo *root, + RelOptInfo *input_rel, + RelOptInfo *final_rel, + FinalPathExtraData *extra); +static void add_foreign_final_paths(PlannerInfo *root, + RelOptInfo *input_rel, + RelOptInfo *final_rel, + FinalPathExtraData *extra); + /* Hash table mapping oid to fdw instances */ HTAB *InstancesHash; @@ -174,6 +189,7 @@ multicorn_handler(PG_FUNCTION_ARGS) /* Plan phase */ fdw_routine->GetForeignRelSize = multicornGetForeignRelSize; fdw_routine->GetForeignPaths = multicornGetForeignPaths; + fdw_routine->GetForeignUpperPaths = multicornGetForeignUpperPaths; fdw_routine->GetForeignPlan = multicornGetForeignPlan; fdw_routine->ExplainForeignScan = multicornExplainForeignScan; @@ -390,6 +406,10 @@ multicornGetForeignPaths(PlannerInfo *root, } } + /* Determine if the sort is completely pushed down and store the results to be used in the upper paths */ + /* Regardless, store the deparsed pathkeys to be used in the upper paths */ + planstate->sort_pushed_down = pathkeys_contained_in(root->sort_pathkeys, apply_pathkeys); + /* Add each ForeignPath previously found */ foreach(lc, pathes) { @@ -403,6 +423,13 @@ multicornGetForeignPaths(PlannerInfo *root, { ForeignPath *newpath; + MulticornPathState *pathstate = (MulticornPathState *)palloc0(sizeof(MulticornPathState)); + pathstate->pathkeys = deparsed_pathkeys; + pathstate->limit = -1; + pathstate->offset = -1; + + planstate->pathkeys = deparsed_pathkeys; + newpath = create_foreignscan_path(root, baserel, NULL, /* default pathtarget */ path->path.rows, @@ -415,7 +442,7 @@ multicornGetForeignPaths(PlannerInfo *root, #if PG_VERSION_NUM >= 170000 NULL, #endif - (void *) deparsed_pathkeys); + (void *)pathstate); newpath->path.param_info = path->path.param_info; add_path(baserel, (Path *) newpath); @@ -424,6 +451,144 @@ multicornGetForeignPaths(PlannerInfo *root, errorCheck(); } +/* + * multicornGetForeignUpperPaths + * Add paths for post-join operations like aggregation, grouping etc. if + * corresponding operations are safe to push down. + * + * Right now, we only support limit/offset pushdown. We'll add others later. + */ +static void multicornGetForeignUpperPaths(PlannerInfo *root, + UpperRelationKind stage, + RelOptInfo *input_rel, + RelOptInfo *output_rel, + void *extra) +{ + // If the input_rel has no private, then pushdown wasn't supported for the previous stage. + // Therefore we can't pushdown anything for the the current stage (as least this is true for limit/offset) + if (!input_rel->fdw_private) + return; + + switch (stage) + { + case UPPERREL_ORDERED: + add_foreign_ordered_paths(root, input_rel, output_rel, (FinalPathExtraData *)extra); + break; + + case UPPERREL_FINAL: + add_foreign_final_paths(root, input_rel, output_rel, (FinalPathExtraData *)extra); + break; + + default: + break; + } +} + +/* + * add_foreign_ordered_paths + * Add foreign paths for performing the sort processing remotely. + * + * Note: Since sorts are already taken care of in the base rel, we only check for pushdown here. + */ + static void + add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel, + RelOptInfo *final_rel, + FinalPathExtraData *extra) + { + MulticornPlanState *planstate = input_rel->fdw_private; + + if ( planstate && planstate->sort_pushed_down ) + { + planstate->input_rel = input_rel; + final_rel->fdw_private = planstate; + } +} + +/* + * add_foreign_final_paths + * Add foreign paths for performing the final processing remotely. + * + * Given input_rel contains the source-data Paths. The paths are added to the + * given final_rel. + */ + static void + add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel, + RelOptInfo *final_rel, + FinalPathExtraData *extra) + { + Query *parse = root->parse; + MulticornPathState *pathstate; + MulticornPlanState *planstate; + ForeignPath *final_path; + int limitCount = -1; + int limitOffset = -1; + + /* No work if there is no need to add a LIMIT node */ + if (!extra->limit_needed) + return; + + /* We only support limits for SELECT commands */ + if (parse->commandType != CMD_SELECT) + return; + + /* We do not support pushing down FETCH FIRST .. WITH TIES */ + if (parse->limitOption == LIMIT_OPTION_WITH_TIES) + return; + + /* We don't currently support pushing down limits with quals */ + if (parse->jointree->quals) + return; + + /* only push down constant LIMITs... */ + if ((parse->limitCount && !IsA(parse->limitCount, Const)) || (parse->limitOffset && !IsA(parse->limitOffset, Const))) + return; + + /* ... which are not NULL */ + if((parse->limitCount && ((Const *)parse->limitCount)->constisnull) || (parse->limitOffset && ((Const *)parse->limitOffset)->constisnull)) + return; + + /* Extract the limit and offset */ + if (parse->limitCount) + limitCount = DatumGetInt32(((Const *)parse->limitCount)->constvalue); + + if (parse->limitOffset) + limitOffset = DatumGetInt32(((Const *)parse->limitOffset)->constvalue); + + /* Get the current planstate and its input_rel */ + planstate = input_rel->fdw_private; + if ( planstate->input_rel ) + input_rel = planstate->input_rel; + + /* Check if Python FWD can push down the LIMIT/OFFSET */ + if (!canLimit(planstate, limitCount, limitOffset)) + return; + + /* Include pathkeys and limit/offset in pathstate */ + pathstate = (MulticornPathState *)palloc(sizeof(MulticornPathState)); + pathstate->pathkeys = planstate->pathkeys; + pathstate->limit = limitCount; + pathstate->offset = limitOffset; + + /* Create foreign final path with the correct number of rows and cost. */ + final_path = create_foreign_upper_path(root, + input_rel, + root->upper_targets[UPPERREL_FINAL], + limitCount, +#if PG_VERSION_NUM >= 180000 // # of disabled_nodes added in PG 18 + 0, +#endif + planstate->startupCost, + limitCount * planstate->width, + NULL, /* pathkeys will be applied in the input_rel */ + NULL, /* no extra plan */ +#if PG_VERSION_NUM >= 170000 + NULL, /* no fdw_restrictinfo list */ +#endif + (void*)pathstate); + /* and add it to the final_rel */ + add_path(final_rel, (Path *) final_path); +} + /* * multicornGetForeignPlan * Create a ForeignScan plan node for scanning the foreign table @@ -458,7 +623,21 @@ multicornGetForeignPlan(PlannerInfo *root, &planstate->qual_list); } } - planstate->pathkeys = (List *) best_path->fdw_private; + + if (best_path->fdw_private) + { + MulticornPathState *pathstate = (MulticornPathState *) best_path->fdw_private; + planstate->pathkeys = pathstate->pathkeys; + planstate->limit = pathstate->limit; + planstate->offset = pathstate->offset; + } + else + { + planstate->pathkeys = NIL; + planstate->limit = -1; + planstate->offset = -1; + } + return make_foreignscan(tlist, scan_clauses, scan_relid, @@ -1162,13 +1341,19 @@ serializePlanState(MulticornPlanState * state) List *result = NULL; result = lappend(result, makeConst(INT4OID, - -1, InvalidOid, 4, Int32GetDatum(state->numattrs), false, true)); + -1, InvalidOid, 4, Int32GetDatum(state->numattrs), false, true)); result = lappend(result, makeConst(INT4OID, -1, InvalidOid, 4, Int32GetDatum(state->foreigntableid), false, true)); result = lappend(result, state->target_list); result = lappend(result, serializeDeparsedSortGroup(state->pathkeys)); + result = lappend(result, makeConst(INT4OID, + -1, InvalidOid, 4, Int32GetDatum(state->limit), false, true)); + + result = lappend(result, makeConst(INT4OID, + -1, InvalidOid, 4, Int32GetDatum(state->offset), false, true)); + return result; } @@ -1195,5 +1380,7 @@ initializeExecState(void *internalstate) execstate->cinfos = palloc0(sizeof(ConversionInfo *) * attnum); execstate->values = palloc(attnum * sizeof(Datum)); execstate->nulls = palloc(attnum * sizeof(bool)); + execstate->limit = DatumGetInt32(((Const*)list_nth(values,4))->constvalue); + execstate->offset = DatumGetInt32(((Const*)list_nth(values,5))->constvalue); return execstate; } diff --git a/src/multicorn.h b/src/multicorn.h index c59f7b8..c5a74f9 100644 --- a/src/multicorn.h +++ b/src/multicorn.h @@ -62,6 +62,12 @@ typedef struct ConversionInfo bool need_quote; } ConversionInfo; +typedef struct MulticornPathState +{ + int offset; + int limit; + List *pathkeys; /* list of MulticornDeparsedSortGroup */ +} MulticornPathState; typedef struct MulticornPlanState { @@ -82,6 +88,17 @@ typedef struct MulticornPlanState * getRelSize to GetForeignPlan. */ int width; + + /* For limit/offset pushdown */ + int offset; + int limit; + + /* For tracking if the sort is completely pushed down */ + bool sort_pushed_down; + + /* Used for tracking the input_rel for upper plans*/ + RelOptInfo *input_rel; + } MulticornPlanState; typedef struct MulticornExecState @@ -100,6 +117,8 @@ typedef struct MulticornExecState AttrNumber rowidAttno; char *rowidAttrName; List *pathkeys; /* list of MulticornDeparsedSortGroup) */ + int limit; + int offset; } MulticornExecState; typedef struct MulticornModifyState @@ -182,6 +201,7 @@ void getRelSize(MulticornPlanState * state, List *pathKeys(MulticornPlanState * state); List *canSort(MulticornPlanState * state, List *deparsed); +bool canLimit(MulticornPlanState * state, int limit, int offset); CacheEntry *getCacheEntry(Oid foreigntableid); UserMapping *multicorn_GetUserMapping(Oid userid, Oid serverid); diff --git a/src/python.c b/src/python.c index c7b0a42..49f110d 100644 --- a/src/python.c +++ b/src/python.c @@ -980,6 +980,16 @@ execute(ForeignScanState *node, ExplainState *es) if(PyList_Size(p_pathkeys) > 0){ PyDict_SetItemString(kwargs, "sortkeys", p_pathkeys); } + if(state->limit >= 0){ + PyObject * limit = PyLong_FromLong((long)state->limit); + PyDict_SetItemString(kwargs, "limit", limit); + Py_DECREF(limit); + } + if(state->offset >=0){ + PyObject * offset = PyLong_FromLong((long)state->offset); + PyDict_SetItemString(kwargs, "offset", offset); + Py_DECREF(offset); + } if(es != NULL){ PyObject * verbose; if(es->verbose){ @@ -1613,6 +1623,29 @@ pathKeys(MulticornPlanState * state) return result; } +/* + * Call the can_limit method from the python implementation and return the result. + */ +bool canLimit(MulticornPlanState * state, int limit, int offset) +{ + PyObject *py_limit = (limit < 0) ? Py_None : PyLong_FromLong(limit); + PyObject *py_offset = (offset < 0) ? Py_None : PyLong_FromLong(offset); + PyObject *result = PyObject_CallMethod(state->fdw_instance, "can_limit", "OO", py_limit, py_offset); + + // Only DECREF if we created new objects + if (limit >= 0) Py_DECREF(py_limit); + if (offset >= 0) Py_DECREF(py_offset); + + if (result) { + int is_true = PyObject_IsTrue(result); + Py_DECREF(result); + return (is_true == 1); + } + + errorCheck(); + return false; +} + /* * Call the can_sort method from the python implementation. We provide a deparsed * version of the requested fields to sort with all detail as needed (nulls, diff --git a/test-3.9/expected/multicorn_test_limit.out b/test-3.9/expected/multicorn_test_limit.out new file mode 100644 index 0000000..2fc094e --- /dev/null +++ b/test-3.9/expected/multicorn_test_limit.out @@ -0,0 +1,230 @@ +SET client_min_messages=NOTICE; +CREATE EXTENSION multicorn; +CREATE server multicorn_srv foreign data wrapper multicorn options ( + wrapper 'multicorn.testfdw.TestForeignDataWrapper' +); +CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); +CREATE foreign table testmulticorn ( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'true' +); +CREATE foreign table testmulticorn_nolimit( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'false', + cansort 'true' +); +CREATE foreign table testmulticorn_nosort( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'false' +); +-- Verify limit and offset are not pushed down when FWD says "no" (but still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'false'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are pushed down when FWD says "yes" (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort not pushed down (still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort are pushed down (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is not pushed down +EXPLAIN SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'false'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +----------------------------------------------------------------------------------------- + Limit (cost=400.20..400.20 rows=1 width=12) + -> Sort (cost=400.20..400.25 rows=20 width=12) + Sort Key: test1 + -> Foreign Scan on testmulticorn_nosort (cost=10.00..400.00 rows=20 width=20) +(4 rows) + +SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is partially pushed down +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=48.08..66.65 rows=1 width=12) + -> Incremental Sort (cost=29.51..400.90 rows=20 width=12) + Sort Key: test1, test2 + Presorted Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) +(5 rows) + +SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with where (which we may eventually support in the future) +EXPLAIN SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------- + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) + Filter: (test1 = '02-03-2011'::date) +(3 rows) + +SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; +NOTICE: [test1 = 2011-02-03] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with group by +EXPLAIN SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=19.52..29.03 rows=1 width=8) + -> GroupAggregate (cost=10.00..200.30 rows=20 width=8) + Group Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(4 rows) + +SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 02-03-2011 +(1 row) + +-- Verify limit and offset are not pushed down with window functions +EXPLAIN SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=28.55..37.59 rows=1 width=8) + -> WindowAgg (cost=19.52..200.30 rows=20 width=8) + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(3 rows) + +SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 01-01-2011 +(1 row) + +-- Verify limit and offset are not pushed down with joins +-- TODO: optimize by pushing down limit/offset on one side of the join +EXPLAIN SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------------- + Limit (cost=59.30..98.61 rows=1 width=24) + -> Nested Loop (cost=20.00..806.05 rows=20 width=24) + Join Filter: (m1.test1 = m2.test1) + -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) + -> Materialize (cost=10.00..400.10 rows=20 width=20) + -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) +(6 rows) + +SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 | test1 | test2 +------------+--------------------------+------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 | 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +DROP USER MAPPING FOR current_user SERVER multicorn_srv; +DROP EXTENSION multicorn cascade; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to server multicorn_srv +drop cascades to foreign table testmulticorn +drop cascades to foreign table testmulticorn_nolimit +drop cascades to foreign table testmulticorn_nosort diff --git a/test-3.9/expected/multicorn_test_limit_1.out b/test-3.9/expected/multicorn_test_limit_1.out new file mode 100644 index 0000000..5b6d2f0 --- /dev/null +++ b/test-3.9/expected/multicorn_test_limit_1.out @@ -0,0 +1,230 @@ +SET client_min_messages=NOTICE; +CREATE EXTENSION multicorn; +CREATE server multicorn_srv foreign data wrapper multicorn options ( + wrapper 'multicorn.testfdw.TestForeignDataWrapper' +); +CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); +CREATE foreign table testmulticorn ( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'true' +); +CREATE foreign table testmulticorn_nolimit( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'false', + cansort 'true' +); +CREATE foreign table testmulticorn_nosort( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'false' +); +-- Verify limit and offset are not pushed down when FWD says "no" (but still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'false'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are pushed down when FWD says "yes" (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort not pushed down (still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort are pushed down (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is not pushed down +EXPLAIN SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'false'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +----------------------------------------------------------------------------------------- + Limit (cost=400.20..400.20 rows=1 width=12) + -> Sort (cost=400.20..400.25 rows=20 width=12) + Sort Key: test1 + -> Foreign Scan on testmulticorn_nosort (cost=10.00..400.00 rows=20 width=20) +(4 rows) + +SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is partially pushed down +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=48.08..66.65 rows=1 width=12) + -> Incremental Sort (cost=29.51..400.90 rows=20 width=12) + Sort Key: test1, test2 + Presorted Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) +(5 rows) + +SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with where (which we may eventually support in the future) +EXPLAIN SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------- + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) + Filter: (test1 = '02-03-2011'::date) +(3 rows) + +SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; +NOTICE: [test1 = 2011-02-03] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with group by +EXPLAIN SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=19.52..29.03 rows=1 width=8) + -> GroupAggregate (cost=10.00..200.30 rows=20 width=8) + Group Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(4 rows) + +SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 02-03-2011 +(1 row) + +-- Verify limit and offset are not pushed down with window functions +EXPLAIN SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=19.52..29.03 rows=1 width=8) + -> WindowAgg (cost=10.00..200.30 rows=20 width=8) + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(3 rows) + +SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 01-01-2011 +(1 row) + +-- Verify limit and offset are not pushed down with joins +-- TODO: optimize by pushing down limit/offset on one side of the join +EXPLAIN SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------------- + Limit (cost=59.30..98.61 rows=1 width=24) + -> Nested Loop (cost=20.00..806.05 rows=20 width=24) + Join Filter: (m1.test1 = m2.test1) + -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) + -> Materialize (cost=10.00..400.10 rows=20 width=20) + -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) +(6 rows) + +SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 | test1 | test2 +------------+--------------------------+------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 | 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +DROP USER MAPPING FOR current_user SERVER multicorn_srv; +DROP EXTENSION multicorn cascade; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to server multicorn_srv +drop cascades to foreign table testmulticorn +drop cascades to foreign table testmulticorn_nolimit +drop cascades to foreign table testmulticorn_nosort diff --git a/test-3.9/expected/multicorn_test_limit_2.out b/test-3.9/expected/multicorn_test_limit_2.out new file mode 100644 index 0000000..389cf38 --- /dev/null +++ b/test-3.9/expected/multicorn_test_limit_2.out @@ -0,0 +1,231 @@ +SET client_min_messages=NOTICE; +CREATE EXTENSION multicorn; +CREATE server multicorn_srv foreign data wrapper multicorn options ( + wrapper 'multicorn.testfdw.TestForeignDataWrapper' +); +CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); +CREATE foreign table testmulticorn ( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'true' +); +CREATE foreign table testmulticorn_nolimit( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'false', + cansort 'true' +); +CREATE foreign table testmulticorn_nosort( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'false' +); +-- Verify limit and offset are not pushed down when FWD says "no" (but still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'false'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are pushed down when FWD says "yes" (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'true'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort not pushed down (still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------ + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn_nolimit (cost=10.00..400.00 rows=20 width=20) +(2 rows) + +SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset w/ sort are pushed down (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +-------------------------------------------------------------------- + Foreign Scan on testmulticorn (cost=10.00..20.00 rows=1 width=20) +(1 row) + +SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is not pushed down +EXPLAIN SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [('canlimit', 'true'), ('cansort', 'false'), ('option1', 'option1'), ('test_type', 'date'), ('usermapping', 'test')] +NOTICE: [('test1', 'date'), ('test2', 'timestamp without time zone')] + QUERY PLAN +----------------------------------------------------------------------------------------- + Limit (cost=400.20..400.20 rows=1 width=20) + -> Sort (cost=400.20..400.25 rows=20 width=20) + Sort Key: test1 + -> Foreign Scan on testmulticorn_nosort (cost=10.00..400.00 rows=20 width=20) +(4 rows) + +SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed when sort is partially pushed down +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=48.08..66.65 rows=1 width=20) + -> Incremental Sort (cost=29.51..400.90 rows=20 width=20) + Sort Key: test1, test2 + Presorted Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) +(5 rows) + +SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + test1 | test2 +------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with where (which we may eventually support in the future) +EXPLAIN SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------- + Limit (cost=29.50..49.00 rows=1 width=20) + -> Foreign Scan on testmulticorn (cost=10.00..400.00 rows=20 width=20) + Filter: (test1 = '02-03-2011'::date) +(3 rows) + +SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; +NOTICE: [test1 = 2011-02-03] +NOTICE: ['test1', 'test2'] + test1 | test2 +------------+-------------------------- + 02-03-2011 | Tue Feb 01 14:30:25 2011 +(1 row) + +-- Verify limit and offset are not pushed down with group by +EXPLAIN SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=19.52..29.03 rows=1 width=8) + -> GroupAggregate (cost=10.00..200.30 rows=20 width=8) + Group Key: test1 + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(4 rows) + +SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 02-03-2011 +(1 row) + +-- Verify limit and offset are not pushed down with window functions +EXPLAIN SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; + QUERY PLAN +---------------------------------------------------------------------------------- + Limit (cost=28.55..37.59 rows=1 width=8) + -> WindowAgg (cost=19.52..200.30 rows=20 width=8) + Window: w1 AS (ORDER BY test1) + -> Foreign Scan on testmulticorn (cost=10.00..200.00 rows=20 width=10) +(4 rows) + +SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1'] +NOTICE: requested sort(s): +NOTICE: SortKey(attname='test1', attnum=1, is_reversed=False, nulls_first=False, collate=None) + max +------------ + 01-01-2011 +(1 row) + +-- Verify limit and offset are not pushed down with joins +-- TODO: optimize by pushing down limit/offset on one side of the join +EXPLAIN SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; + QUERY PLAN +------------------------------------------------------------------------------------------- + Limit (cost=59.30..98.61 rows=1 width=24) + -> Nested Loop (cost=20.00..806.05 rows=20 width=24) + Join Filter: (m1.test1 = m2.test1) + -> Foreign Scan on testmulticorn m1 (cost=10.00..400.00 rows=20 width=20) + -> Materialize (cost=10.00..400.10 rows=20 width=20) + -> Foreign Scan on testmulticorn m2 (cost=10.00..400.00 rows=20 width=20) +(6 rows) + +SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; +NOTICE: [] +NOTICE: ['test1', 'test2'] +NOTICE: [] +NOTICE: ['test1', 'test2'] + test1 | test2 | test1 | test2 +------------+--------------------------+------------+-------------------------- + 01-01-2011 | Sun Jan 02 14:30:25 2011 | 01-01-2011 | Sun Jan 02 14:30:25 2011 +(1 row) + +DROP USER MAPPING FOR current_user SERVER multicorn_srv; +DROP EXTENSION multicorn cascade; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to server multicorn_srv +drop cascades to foreign table testmulticorn +drop cascades to foreign table testmulticorn_nolimit +drop cascades to foreign table testmulticorn_nosort diff --git a/test-3.9/sql/multicorn_test_limit.sql b/test-3.9/sql/multicorn_test_limit.sql new file mode 100644 index 0000000..57d7934 --- /dev/null +++ b/test-3.9/sql/multicorn_test_limit.sql @@ -0,0 +1,80 @@ +SET client_min_messages=NOTICE; +CREATE EXTENSION multicorn; +CREATE server multicorn_srv foreign data wrapper multicorn options ( + wrapper 'multicorn.testfdw.TestForeignDataWrapper' +); +CREATE user mapping FOR current_user server multicorn_srv options (usermapping 'test'); + +CREATE foreign table testmulticorn ( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'true' +); + +CREATE foreign table testmulticorn_nolimit( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'false', + cansort 'true' +); + +CREATE foreign table testmulticorn_nosort( + test1 date, + test2 timestamp +) server multicorn_srv options ( + option1 'option1', + test_type 'date', + canlimit 'true', + cansort 'false' +); + +-- Verify limit and offset are not pushed down when FWD says "no" (but still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn_nolimit LIMIT 1 OFFSET 1; + +-- Verify limit and offset are pushed down when FWD says "yes" (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn LIMIT 1 OFFSET 1; + +-- Verify limit and offset w/ sort not pushed down (still returns the 1 correct row) +EXPLAIN SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn_nolimit ORDER BY test1 LIMIT 1 OFFSET 1; + +-- Verify limit and offset w/ sort are pushed down (returns just 1 row with proper cost) +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed when sort is not pushed down +EXPLAIN SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn_nosort ORDER BY test1 LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed when sort is partially pushed down +EXPLAIN SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn ORDER BY test1, test2 LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed down with where (which we may eventually support in the future) +EXPLAIN SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn WHERE test1 = '02-03-2011' LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed down with group by +EXPLAIN SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; +SELECT max(test1) FROM testmulticorn GROUP BY test1 LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed down with window functions +EXPLAIN SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; +SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSET 1; + +-- Verify limit and offset are not pushed down with joins +-- TODO: optimize by pushing down limit/offset on one side of the join +EXPLAIN SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; +SELECT * FROM testmulticorn m1 JOIN testmulticorn m2 ON m1.test1 = m2.test1 LIMIT 1 OFFSET 1; + +DROP USER MAPPING FOR current_user SERVER multicorn_srv; +DROP EXTENSION multicorn cascade;