From b5b5814f03eef98deb93d4f69001e5686ba94d61 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Wed, 3 Sep 2025 12:47:46 -0700 Subject: [PATCH 1/7] Initial implementation --- Makefile | 1 + flake.nix | 2 +- python/multicorn/__init__.py | 21 ++- python/multicorn/testfdw.py | 24 ++- src/multicorn.c | 191 ++++++++++++++++++++- src/multicorn.h | 14 ++ src/python.c | 33 ++++ test-3.9/expected/multicorn_test_limit.out | 130 ++++++++++++++ test-3.9/sql/multicorn_test_limit.sql | 59 +++++++ 9 files changed, 462 insertions(+), 13 deletions(-) create mode 100644 test-3.9/expected/multicorn_test_limit.out create mode 100644 test-3.9/sql/multicorn_test_limit.sql 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..2fd22de 100755 --- a/python/multicorn/__init__.py +++ b/python/multicorn/__init__.py @@ -212,6 +212,21 @@ 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 pushdown. + This method is only called if the rest of the query can be pushed down including the sort and quals. For example, + if the query has a GROUP BY clause, this method will not be called. + + Args: + limit (int or None): The limit to apply to the query. + offset (int or None): The offset to apply to the query. + + Return: + True if the FDW can support both LIMIT and OFFSET pushdown, Falseotherwise. + """ + return False + def get_path_keys(self): u""" Method called from the planner to add additional Path to the planner. @@ -269,7 +284,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 +295,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 +328,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): The limit to apply to the query. + offset (int): The offset to apply to the query. 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..fe2c465 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),)) @@ -127,8 +134,13 @@ def get_path_keys(self): def can_sort(self, sortkeys): # assume sort pushdown ok for all cols, in any order, any collation + if not self.cansort: + return [] return sortkeys + def can_limit(self, limit, offset): + return self.canlimit + def update(self, rowid, newvalues): if self.test_type == 'nowrite': super(TestForeignDataWrapper, self).update(rowid, newvalues) diff --git a/src/multicorn.c b/src/multicorn.c index 2d9c8d0..3c0cc23 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,11 @@ static void multicorn_xact_callback(XactEvent event, void *arg); void *serializePlanState(MulticornPlanState * planstate); MulticornExecState *initializeExecState(void *internal_plan_state); +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 +185,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; @@ -359,7 +371,7 @@ multicornGetForeignPaths(PlannerInfo *root, /* Try to find parameterized paths */ pathes = findPaths(root, baserel, possiblePaths, planstate->startupCost, planstate, apply_pathkeys, deparsed_pathkeys); - + /* Add a simple default path */ pathes = lappend(pathes, create_foreignscan_path(root, baserel, NULL, /* default pathtarget */ @@ -403,6 +415,11 @@ multicornGetForeignPaths(PlannerInfo *root, { ForeignPath *newpath; + MulticornPathState *pathstate = (MulticornPathState *)calloc(1, sizeof(MulticornPathState)); + pathstate->pathkeys = deparsed_pathkeys; + pathstate->limit = -1; + pathstate->offset = -1; + newpath = create_foreignscan_path(root, baserel, NULL, /* default pathtarget */ path->path.rows, @@ -415,7 +432,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 +441,150 @@ 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) +{ + switch (stage) + { + case UPPERREL_FINAL: + add_foreign_final_paths(root, input_rel, output_rel, (FinalPathExtraData *)extra); + break; + default: + break; + } +} + +/* + * 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; + Path *cheapest_path; + List *deparsed_pathkeys = NIL; + List *applied_pathkeys = NIL; + ListCell *lc; + + /* 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; + + /* 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 input_rel and it's planstate */ + planstate = input_rel->fdw_private; + // TODO: Maybe this isn't needed if we handle the previous stages correctly? + if (!planstate) + { + for (int i = 1; i < root->simple_rel_array_size; i++) + { + RelOptInfo *rel = root->simple_rel_array[i]; + if (rel && rel->reloptkind == RELOPT_BASEREL) + { + planstate = rel->fdw_private; + input_rel->fdw_private = planstate; + input_rel = rel; + break; + } + } + } + + /* Extract pathkeys from the cheapest path's fdw_private if it exists */ + cheapest_path = input_rel->cheapest_total_path; + if (cheapest_path && IsA(cheapest_path, ForeignPath)) + { + ForeignPath *foreign_path = (ForeignPath *)cheapest_path; + if (foreign_path->fdw_private) + { + MulticornPathState *input_pathstate = (MulticornPathState *)foreign_path->fdw_private; + deparsed_pathkeys = input_pathstate->pathkeys; + } + } + + /* Extract the pathkeys from the input_rel */ + foreach(lc, input_rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + if (IsA(path, ForeignPath)) + { + ForeignPath *fpath = (ForeignPath *) path; + if (fpath->path.pathkeys != NIL) + applied_pathkeys = fpath->path.pathkeys; + } + } + + /* We only support limit/offset if the sort is completely pushed down */ + if (!pathkeys_contained_in(root->sort_pathkeys, applied_pathkeys)) + return; + + /* Check if Python FWD can push down the LIMIT/OFFSET */ + if (!canLimit(planstate, limitCount, limitOffset)) + return; + + /* Create foreign final path with the correct number of rows, and include state for limit/offset pushdown */ + pathstate = (MulticornPathState *)calloc(1, sizeof(MulticornPathState)); + pathstate->pathkeys = deparsed_pathkeys; + pathstate->limit = limitCount; + pathstate->offset = limitOffset; + final_path = create_foreign_upper_path(root, + input_rel, + root->upper_targets[UPPERREL_FINAL], + limitCount, + 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 +619,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 +1337,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 +1376,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..7ed7003 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,11 @@ typedef struct MulticornPlanState * getRelSize to GetForeignPlan. */ int width; + + /* For limit/offset pushdown */ + int offset; + int limit; + } MulticornPlanState; typedef struct MulticornExecState @@ -100,6 +111,8 @@ typedef struct MulticornExecState AttrNumber rowidAttno; char *rowidAttrName; List *pathkeys; /* list of MulticornDeparsedSortGroup) */ + int limit; + int offset; } MulticornExecState; typedef struct MulticornModifyState @@ -182,6 +195,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..70b57ec --- /dev/null +++ b/test-3.9/expected/multicorn_test_limit.out @@ -0,0 +1,130 @@ +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) + +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..1e48935 --- /dev/null +++ b/test-3.9/sql/multicorn_test_limit.sql @@ -0,0 +1,59 @@ +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; + +DROP USER MAPPING FOR current_user SERVER multicorn_srv; +DROP EXTENSION multicorn cascade; From 6cca844debc5ef2bd009d4bf7cff573c091301cc Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Thu, 4 Sep 2025 09:29:26 -0700 Subject: [PATCH 2/7] Fix build error on PG18 --- src/multicorn.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/multicorn.c b/src/multicorn.c index 3c0cc23..d15bdee 100644 --- a/src/multicorn.c +++ b/src/multicorn.c @@ -573,6 +573,9 @@ static void multicornGetForeignUpperPaths(PlannerInfo *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 */ From 2f27ecd46dd40988948e782973b965bee7129c02 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Fri, 5 Sep 2025 13:02:32 -0700 Subject: [PATCH 3/7] Add more tests and prevent push down for where, group by, over, etc. --- src/multicorn.c | 125 +++++++++++++-------- src/multicorn.h | 3 + test-3.9/expected/multicorn_test_limit.out | 79 +++++++++++++ test-3.9/sql/multicorn_test_limit.sql | 17 +++ 4 files changed, 176 insertions(+), 48 deletions(-) diff --git a/src/multicorn.c b/src/multicorn.c index d15bdee..00aac27 100644 --- a/src/multicorn.c +++ b/src/multicorn.c @@ -125,6 +125,10 @@ 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, @@ -454,8 +458,20 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, RelOptInfo *output_rel, void *extra) { + // elog(WARNING, "Got input_rel private: %p", input_rel->fdw_private); + // elog(WARNING, "Got output_rel private: %p", output_rel->fdw_private); + + // If the input_rel has no private, then pushdown wasn't supported for the previous stage + // Which means we can't pushdown anything for the the current stage (as least this is true for limit/offset) + if (!input_rel->fdw_private) + return; + + // elog(WARNING, "Got stage: %d", stage); 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; @@ -464,6 +480,60 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, } } +/* + * add_foreign_ordered_paths + * Add foreign paths for performing the sort processing remotely. + * + * Given input_rel contains the source-data Paths. The paths are added to the + * given final_rel. + * + * 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) + { + List *applied_pathkeys = NIL; + ListCell *lc; + MulticornPlanState *planstate = input_rel->fdw_private; + ForeignPath *cheapest_path = (ForeignPath *)input_rel->cheapest_total_path; + if ( planstate ) + { + if (cheapest_path && IsA(cheapest_path, ForeignPath)) + { + MulticornPathState *pathstate = (MulticornPathState *)cheapest_path->fdw_private; + if ( pathstate ) + { + planstate->pathkeys = pathstate->pathkeys; + } + } + + /* Extract the pathkeys from the input_rel */ + foreach(lc, input_rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + if (IsA(path, ForeignPath)) + { + ForeignPath *fpath = (ForeignPath *) path; + if (fpath->path.pathkeys != NIL) + { + applied_pathkeys = fpath->path.pathkeys; + break; + } + } + } + + /* We only support limit/offset if the sort is completely pushed down */ + /* By bailing here, input_rel for the next state will not have planstate, which will cause no more pushdowns */ + if (!pathkeys_contained_in(root->sort_pathkeys, applied_pathkeys)) + return; + + planstate->input_rel = input_rel; + final_rel->fdw_private = planstate; + } +} + /* * add_foreign_final_paths * Add foreign paths for performing the final processing remotely. @@ -482,10 +552,6 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, ForeignPath *final_path; int limitCount = -1; int limitOffset = -1; - Path *cheapest_path; - List *deparsed_pathkeys = NIL; - List *applied_pathkeys = NIL; - ListCell *lc; /* No work if there is no need to add a LIMIT node */ if (!extra->limit_needed) @@ -499,6 +565,10 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, 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; @@ -516,49 +586,8 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, /* Get the current input_rel and it's planstate */ planstate = input_rel->fdw_private; - // TODO: Maybe this isn't needed if we handle the previous stages correctly? - if (!planstate) - { - for (int i = 1; i < root->simple_rel_array_size; i++) - { - RelOptInfo *rel = root->simple_rel_array[i]; - if (rel && rel->reloptkind == RELOPT_BASEREL) - { - planstate = rel->fdw_private; - input_rel->fdw_private = planstate; - input_rel = rel; - break; - } - } - } - - /* Extract pathkeys from the cheapest path's fdw_private if it exists */ - cheapest_path = input_rel->cheapest_total_path; - if (cheapest_path && IsA(cheapest_path, ForeignPath)) - { - ForeignPath *foreign_path = (ForeignPath *)cheapest_path; - if (foreign_path->fdw_private) - { - MulticornPathState *input_pathstate = (MulticornPathState *)foreign_path->fdw_private; - deparsed_pathkeys = input_pathstate->pathkeys; - } - } - - /* Extract the pathkeys from the input_rel */ - foreach(lc, input_rel->pathlist) - { - Path *path = (Path *) lfirst(lc); - if (IsA(path, ForeignPath)) - { - ForeignPath *fpath = (ForeignPath *) path; - if (fpath->path.pathkeys != NIL) - applied_pathkeys = fpath->path.pathkeys; - } - } - - /* We only support limit/offset if the sort is completely pushed down */ - if (!pathkeys_contained_in(root->sort_pathkeys, applied_pathkeys)) - return; + if ( planstate->input_rel ) + input_rel = planstate->input_rel; /* Check if Python FWD can push down the LIMIT/OFFSET */ if (!canLimit(planstate, limitCount, limitOffset)) @@ -566,7 +595,7 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, /* Create foreign final path with the correct number of rows, and include state for limit/offset pushdown */ pathstate = (MulticornPathState *)calloc(1, sizeof(MulticornPathState)); - pathstate->pathkeys = deparsed_pathkeys; + pathstate->pathkeys = planstate->pathkeys; pathstate->limit = limitCount; pathstate->offset = limitOffset; final_path = create_foreign_upper_path(root, diff --git a/src/multicorn.h b/src/multicorn.h index 7ed7003..7f1c2fc 100644 --- a/src/multicorn.h +++ b/src/multicorn.h @@ -93,6 +93,9 @@ typedef struct MulticornPlanState int offset; int limit; + /* Used for tracking the input_rel for upper plans*/ + RelOptInfo *input_rel; + } MulticornPlanState; typedef struct MulticornExecState diff --git a/test-3.9/expected/multicorn_test_limit.out b/test-3.9/expected/multicorn_test_limit.out index 70b57ec..6f8bf56 100644 --- a/test-3.9/expected/multicorn_test_limit.out +++ b/test-3.9/expected/multicorn_test_limit.out @@ -121,6 +121,85 @@ NOTICE: ['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 diff --git a/test-3.9/sql/multicorn_test_limit.sql b/test-3.9/sql/multicorn_test_limit.sql index 1e48935..cc6ca23 100644 --- a/test-3.9/sql/multicorn_test_limit.sql +++ b/test-3.9/sql/multicorn_test_limit.sql @@ -55,5 +55,22 @@ SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; 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 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; From a7490ab701fe0dc4044295ffa30edba855f140c6 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Sun, 7 Sep 2025 17:52:33 -0700 Subject: [PATCH 4/7] Clean up code and docs. --- python/multicorn/__init__.py | 20 ++++--- python/multicorn/testfdw.py | 4 +- src/multicorn.c | 68 +++++++--------------- src/multicorn.h | 3 + test-3.9/expected/multicorn_test_limit.out | 21 +++++++ test-3.9/sql/multicorn_test_limit.sql | 4 ++ 6 files changed, 62 insertions(+), 58 deletions(-) diff --git a/python/multicorn/__init__.py b/python/multicorn/__init__.py index 2fd22de..9908180 100755 --- a/python/multicorn/__init__.py +++ b/python/multicorn/__init__.py @@ -214,16 +214,20 @@ def can_sort(self, sortkeys): def can_limit(self, limit, offset): """ - Method called from the planner to ask the FDW whether it supports LIMIT pushdown. - This method is only called if the rest of the query can be pushed down including the sort and quals. For example, - if the query has a GROUP BY clause, this method will not be called. + 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. - offset (int or None): The offset to apply to the query. + 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, Falseotherwise. + True if the FDW can support both LIMIT and OFFSET pushdown, False otherwise. """ return False @@ -328,8 +332,8 @@ def execute(self, quals, columns, sortkeys=None, limit=None, offset=None): should be in the sequence. sortkeys (list): A list of :class:`SortKey` that the FDW said it can enforce. - limit (int): The limit to apply to the query. - offset (int): The offset to apply to the query. + 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 fe2c465..598eed6 100644 --- a/python/multicorn/testfdw.py +++ b/python/multicorn/testfdw.py @@ -133,10 +133,10 @@ def get_path_keys(self): return [] def can_sort(self, sortkeys): - # assume sort pushdown ok for all cols, in any order, any collation + # assume sort pushdown ok only for first sort key if not self.cansort: return [] - return sortkeys + return sortkeys[:1] def can_limit(self, limit, offset): return self.canlimit diff --git a/src/multicorn.c b/src/multicorn.c index 00aac27..5ab1ab8 100644 --- a/src/multicorn.c +++ b/src/multicorn.c @@ -375,7 +375,7 @@ multicornGetForeignPaths(PlannerInfo *root, /* Try to find parameterized paths */ pathes = findPaths(root, baserel, possiblePaths, planstate->startupCost, planstate, apply_pathkeys, deparsed_pathkeys); - + /* Add a simple default path */ pathes = lappend(pathes, create_foreignscan_path(root, baserel, NULL, /* default pathtarget */ @@ -406,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) { @@ -419,11 +423,13 @@ multicornGetForeignPaths(PlannerInfo *root, { ForeignPath *newpath; - MulticornPathState *pathstate = (MulticornPathState *)calloc(1, sizeof(MulticornPathState)); + 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, @@ -458,23 +464,21 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, RelOptInfo *output_rel, void *extra) { - // elog(WARNING, "Got input_rel private: %p", input_rel->fdw_private); - // elog(WARNING, "Got output_rel private: %p", output_rel->fdw_private); - - // If the input_rel has no private, then pushdown wasn't supported for the previous stage - // Which means we can't pushdown anything for the the current stage (as least this is true for limit/offset) + // 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; - // elog(WARNING, "Got stage: %d", stage); 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; } @@ -484,9 +488,6 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, * add_foreign_ordered_paths * Add foreign paths for performing the sort processing remotely. * - * Given input_rel contains the source-data Paths. The paths are added to the - * given final_rel. - * * Note: Since sorts are already taken care of in the base rel, we only check for pushdown here. */ static void @@ -494,41 +495,10 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, RelOptInfo *final_rel, FinalPathExtraData *extra) { - List *applied_pathkeys = NIL; - ListCell *lc; MulticornPlanState *planstate = input_rel->fdw_private; - ForeignPath *cheapest_path = (ForeignPath *)input_rel->cheapest_total_path; - if ( planstate ) - { - if (cheapest_path && IsA(cheapest_path, ForeignPath)) - { - MulticornPathState *pathstate = (MulticornPathState *)cheapest_path->fdw_private; - if ( pathstate ) - { - planstate->pathkeys = pathstate->pathkeys; - } - } - - /* Extract the pathkeys from the input_rel */ - foreach(lc, input_rel->pathlist) - { - Path *path = (Path *) lfirst(lc); - if (IsA(path, ForeignPath)) - { - ForeignPath *fpath = (ForeignPath *) path; - if (fpath->path.pathkeys != NIL) - { - applied_pathkeys = fpath->path.pathkeys; - break; - } - } - } - - /* We only support limit/offset if the sort is completely pushed down */ - /* By bailing here, input_rel for the next state will not have planstate, which will cause no more pushdowns */ - if (!pathkeys_contained_in(root->sort_pathkeys, applied_pathkeys)) - return; + if ( planstate && planstate->sort_pushed_down ) + { planstate->input_rel = input_rel; final_rel->fdw_private = planstate; } @@ -584,7 +554,7 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, if (parse->limitOffset) limitOffset = DatumGetInt32(((Const *)parse->limitOffset)->constvalue); - /* Get the current input_rel and it's planstate */ + /* Get the current planstate and its input_rel */ planstate = input_rel->fdw_private; if ( planstate->input_rel ) input_rel = planstate->input_rel; @@ -593,11 +563,13 @@ static void multicornGetForeignUpperPaths(PlannerInfo *root, if (!canLimit(planstate, limitCount, limitOffset)) return; - /* Create foreign final path with the correct number of rows, and include state for limit/offset pushdown */ - pathstate = (MulticornPathState *)calloc(1, sizeof(MulticornPathState)); + /* 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], @@ -1369,7 +1341,7 @@ 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); diff --git a/src/multicorn.h b/src/multicorn.h index 7f1c2fc..c5a74f9 100644 --- a/src/multicorn.h +++ b/src/multicorn.h @@ -93,6 +93,9 @@ typedef struct MulticornPlanState 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; diff --git a/test-3.9/expected/multicorn_test_limit.out b/test-3.9/expected/multicorn_test_limit.out index 6f8bf56..2fc094e 100644 --- a/test-3.9/expected/multicorn_test_limit.out +++ b/test-3.9/expected/multicorn_test_limit.out @@ -121,6 +121,27 @@ NOTICE: ['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 diff --git a/test-3.9/sql/multicorn_test_limit.sql b/test-3.9/sql/multicorn_test_limit.sql index cc6ca23..57d7934 100644 --- a/test-3.9/sql/multicorn_test_limit.sql +++ b/test-3.9/sql/multicorn_test_limit.sql @@ -55,6 +55,10 @@ SELECT * FROM testmulticorn ORDER BY test1 LIMIT 1 OFFSET 1; 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; From 6ef85b98c4940c76cb2fe31a759d4423b397fd0a Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Tue, 9 Sep 2025 12:49:18 -0700 Subject: [PATCH 5/7] Different expected costs for PG14 --- test-3.9/expected/multicorn_test_limit_1.out | 230 +++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 test-3.9/expected/multicorn_test_limit_1.out 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..3540201 --- /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..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 From a48b9e63120d213673136a1bb26e4eecc3c5c93c Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Tue, 9 Sep 2025 13:10:06 -0700 Subject: [PATCH 6/7] Fix typo --- test-3.9/expected/multicorn_test_limit_1.out | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-3.9/expected/multicorn_test_limit_1.out b/test-3.9/expected/multicorn_test_limit_1.out index 3540201..5b6d2f0 100644 --- a/test-3.9/expected/multicorn_test_limit_1.out +++ b/test-3.9/expected/multicorn_test_limit_1.out @@ -184,7 +184,7 @@ EXPLAIN SELECT max(test1) OVER (ORDER BY test1) FROM testmulticorn LIMIT 1 OFFSE QUERY PLAN ---------------------------------------------------------------------------------- Limit (cost=19.52..29.03 rows=1 width=8) - -> WindowAgg (cost=10..200.30 rows=20 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) From 67b56e8e1dc6aa158c320aee1943214cdcc4c2bc Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Tue, 9 Sep 2025 14:29:58 -0700 Subject: [PATCH 7/7] Fix expected widths/explain for PG18 --- test-3.9/expected/multicorn_test_limit_2.out | 231 +++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 test-3.9/expected/multicorn_test_limit_2.out 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