futurewei-cloud / chogori-sql

Horizontally scalable SQL Service built upon Chogori-Platform SKV storage
Other
5 stars 6 forks source link

Support field references #192

Open iafuture opened 3 years ago

iafuture commented 3 years ago

e.g. UPDATE X SET Field1 = Field2 + 1 WHERE pkf1=1 AND pkf2=2;

jerryhfeng commented 3 years ago

Investigation on design/breakdown first.

johnfangAFW commented 3 years ago

postgres=# explain Update DISTRICT SET D_NEXT_O_ID = D_NEXT_O_ID + 1 WHERE D_W_ID = 1 AND D_ID = 2; QUERY PLAN

Update on district (cost=0.00..4.12 rows=1 width=346) -> Result (cost=0.00..4.12 rows=1 width=346)

(gdb) break ybcExpr.c:YBCNewEvalExprCall Breakpoint 1 at 0x5626834a86f9: file /build/src/k2/postgres/src/backend/executor/ybcExpr.c, line 62.

(gdb) l 57
58 YBCPgExpr YBCNewEvalExprCall(YBCPgStatement ybc_stmt, 59 Expr pg_expr, 60 int32_t attno, 61 int32_t typid, 62 int32_t typmod) { 63 YBCPgExpr ybc_expr = NULL; 64 const YBCPgTypeEntity type_ent = YBCDataTypeFromOidMod(InvalidAttrNumber, typid); 65 YBCPgNewOperator(ybc_stmt, "eval_expr_call", type_ent, &ybc_expr); 66
(gdb) p pg_expr $1 = (Expr ) 0x562685b9aee8 (gdb) p pg_expr $2 = {type = T_OpExpr} (gdb) p attno $3 = 5 (gdb) p typid $4 = 23 (gdb) p typmod $5 = -1 (gdb) n 65 YBCPgNewOperator(ybc_stmt, "eval_expr_call", type_ent, &ybc_expr); (gdb) s k2pg::gate::YBCPgNewOperator (stmt=stmt@entry=0x562685b9c4c0, opname=opname@entry=0x562683d9f4a3 "eval_expr_call", type_entity=0x562684264618 <YBCTypeEntityTable+280>, op_handle=op_handle@entry=0x7ffdaa84b640) at /build/src/k2/connector/pggate/pg_gate_api.cc:878 878 YBCPgExpr *op_handle){ (gdb) n 880 return ToYBCStatus(api_impl->NewOperator(stmt, opname, type_entity, op_handle)); (gdb) s k2pg::gate::PgGateApiImpl::NewOperator (this=0x5626859451d0, stmt=stmt@entry=0x562685b9c4c0, opname=opname@entry=0x562683d9f4a3 "eval_expr_call", type_entity=0x562684264618 <YBCTypeEntityTable+280>, op_handle=op_handle@entry=0x7ffdaa84b640) at /build/src/k2/connector/pggate/pg_gate_impl.cc:813 813 PgExpr **op_handle) {

(gdb) p str $5 = { data = 0x562685b98d08 "{OPEXPR :opno 551 :opfuncid 177 :opresulttype 23 :opretset false :opcollid 0 :inputcollid 0 :args ({VAR :varno 1 :varattno 5 :vartype 23 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattn"..., len = 380, maxlen = 1024, cursor = 0}

(gdb) p (PgConstant )arg $4 = { = {vptr.PgExpr = 0x7f5d9cb2d490 <vtable for k2pg::sql::PgConstant+16>, opcode = k2pg::sql::PgExpr::Opcode::PG_EXPR_CONSTANT, typeentity = 0x558231925ba8 <YBCTypeEntityTable+5800>, typeattrs = {typmod = 0}}, value = {type = k2pg::sql::SqlValue::SLICE, data_ = {{boolval = false, intval = 0, floatval = 0, doubleval = 0}, sliceval = "{OPEXPR :opno 551 :opfuncid 177 :opresulttype 23 :opretset false :opcollid 0 :inputcollid 0 :args ({VAR :varno 1 :varattno 5 :vartype 23 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattn"...}, nullvalue = false}}

(gdb) s _outOpExpr (str=str@entry=0x7ffd7a06f700, node=node@entry=0x55823277ec78) at /build/src/k2/postgres/src/backend/nodes/outfuncs.c:1304 1304 WRITE_NODE_TYPE("OPEXPR"); (gdb) p str $8 = { data = 0x55823277bd58 "{OPEXPR :opno 551 :opfuncid 177 :opresulttype 23 :opretset false :opcollid 0 :inputcollid 0 :args ({VAR :varno 1 :varattno 5 :vartype 23 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattn"..., len = 380, maxlen = 1024, cursor = 0}

johnfangAFW commented 3 years ago

YBCPgExpr YBCNewEvalExprCall(YBCPgStatement ybc_stmt, Expr pg_expr, int32_t attno, int32_t typid, int32_t typmod) { YBCPgExpr ybc_expr = NULL; const YBCPgTypeEntity type_ent = YBCDataTypeFromOidMod(InvalidAttrNumber, typid); YBCPgNewOperator(ybc_stmt, "eval_expr_call", type_ent, &ybc_expr);

Datum expr_datum = CStringGetDatum(nodeToString(pg_expr));
YBCPgExpr expr = YBCNewConstant(ybc_stmt, CSTRINGOID, expr_datum , /* IsNull */ false);
YBCPgOperatorAppendArg(ybc_expr, expr);

/*
 * Adding the column type id and mod to the message since we only have the YQL types in the
 * DocDB Schema.
 * TODO(mihnea): Eventually DocDB should know the full YSQL/PG types and we can remove this.
 */
YBCPgExpr attno_expr = YBCNewConstant(ybc_stmt, INT4OID, (Datum) attno, /* IsNull */ false);
YBCPgOperatorAppendArg(ybc_expr, attno_expr);
YBCPgExpr typid_expr = YBCNewConstant(ybc_stmt, INT4OID, (Datum) typid, /* IsNull */ false);
YBCPgOperatorAppendArg(ybc_expr, typid_expr);
YBCPgExpr typmod_expr = YBCNewConstant(ybc_stmt, INT4OID, (Datum) typmod, /* IsNull */ false);
YBCPgOperatorAppendArg(ybc_expr, typmod_expr);

return ybc_expr;

}

johnfangAFW commented 3 years ago

bool YBCExecuteUpdate(Relation rel, TupleTableSlot slot, HeapTuple tuple, EState estate, ModifyTableState mtstate, Bitmapset updatedCols) { TupleDesc tupleDesc = slot->tts_tupleDescriptor; Oid dboid = YBCGetDatabaseOid(rel); Oid relid = RelationGetRelid(rel); YBCPgStatement update_stmt = NULL; bool isSingleRow = mtstate->yb_mt_is_single_row_update_or_delete; Datum ybctid = 0;

/* Create update statement. */
HandleYBStatus(YBCPgNewUpdate(dboid,
                              relid,
                              estate->es_yb_is_single_row_modify_txn,
                              &update_stmt));

/*
 * Look for ybctid. Raise error if ybctid is not found.
 *
 * If single row update, generate ybctid from tuple values, otherwise
 * retrieve it from the slot.
 */
if (isSingleRow)
{
    ybctid = YBCGetYBTupleIdFromTuple(update_stmt,
                                      rel,
                                      tuple,
                                      slot->tts_tupleDescriptor);
}
else
{
    ybctid = YBCGetYBTupleIdFromSlot(slot);
}

if (ybctid == 0)
{
    ereport(ERROR,
            (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg(
                "Missing column ybctid in UPDATE request to YugaByte database")));
}

/* Bind ybctid to identify the current row. */
YBCPgExpr ybctid_expr = YBCNewConstant(update_stmt, BYTEAOID, ybctid,
                                       false /* is_null */);
HandleYBStatus(YBCPgDmlBindColumn(update_stmt, YBTupleIdAttributeNumber, ybctid_expr));

/* Assign new values to the updated columns for the current row. */
tupleDesc = RelationGetDescr(rel);
bool whole_row = bms_is_member(InvalidAttrNumber, updatedCols);

ModifyTable *mt_plan = (ModifyTable *) mtstate->ps.plan;
ListCell* pushdown_lc = list_head(mt_plan->ybPushdownTlist);

for (int idx = 0; idx < tupleDesc->natts; idx++)
{
    FormData_pg_attribute *att_desc = TupleDescAttr(tupleDesc, idx);

    AttrNumber attnum = att_desc->attnum;
    int32_t type_id = att_desc->atttypid;
    int32_t type_mod = att_desc->atttypmod;

    /* Skip virtual (system) and dropped columns */
    if (!IsRealYBColumn(rel, attnum))
        continue;

    /*
     * Skip unmodified columns if possible.
     * Note: we only do this for the single-row case, as otherwise there
     * might be triggers that modify the heap tuple to set (other) columns
     * (e.g. using the SPI module functions).
     */
    int bms_idx = attnum - YBGetFirstLowInvalidAttributeNumber(rel);
    if (isSingleRow && !whole_row && !bms_is_member(bms_idx, updatedCols))
        continue;

    /* Assign this attr's value, handle expression pushdown if needed. */
    if (pushdown_lc != NULL &&
        ((TargetEntry *) lfirst(pushdown_lc))->resno == attnum)
    {
        TargetEntry *tle = (TargetEntry *) lfirst(pushdown_lc);
        Expr *expr = copyObject(tle->expr);
        YBCExprInstantiateParams(expr, estate->es_param_list_info);

        YBCPgExpr ybc_expr = YBCNewEvalExprCall(update_stmt, expr, attnum, type_id, type_mod);

        HandleYBStatus(YBCPgDmlAssignColumn(update_stmt, attnum, ybc_expr));

        pushdown_lc = lnext(pushdown_lc);
    }
    else
    {
        bool is_null = false;
        Datum d = heap_getattr(tuple, attnum, tupleDesc, &is_null);
        YBCPgExpr ybc_expr = YBCNewConstant(update_stmt, type_id,
                                            d, is_null);

        HandleYBStatus(YBCPgDmlAssignColumn(update_stmt, attnum, ybc_expr));
    }
}

/* Execute the statement. */
int rows_affected_count = 0;
YBCExecWriteStmt(update_stmt, rel, isSingleRow ? &rows_affected_count : NULL);

/* Cleanup. */
update_stmt = NULL;

/*
 * If the relation has indexes, save the ybctid to insert the updated row into the indexes.
 */
if (YBRelHasSecondaryIndices(rel))
{
    tuple->t_ybctid = ybctid;
}

return !isSingleRow || rows_affected_count > 0;

}

johnfangAFW commented 3 years ago

1) PG internal function calls 2) how YB generated the pushdown list 3) Whether YB supported custom function calls

johnfangAFW commented 3 years ago

1) The list of buildint functions inside PG are listed in fmgrtab.c

*

For example for the function in the described SQL: opfuncid 177, it was

{ 177, "int4pl", 2, true, false, int4pl },

which was defined in int.c

as follows

Datum int4pl(PG_FUNCTION_ARGS) { int32 arg1 = PG_GETARG_INT32(0); int32 arg2 = PG_GETARG_INT32(1); int32 result;

if (unlikely(pg_add_s32_overflow(arg1, arg2, &result)))
    ereport(ERROR,
            (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("integer out of range")));
PG_RETURN_INT32(result);

}

johnfangAFW commented 3 years ago

The logic for YB to decide whether it should pushdown functions are as follows as in ybcplan.c

/*

as we could see, it checks whether the function is a built-in one and then it is not a Polymorhipc pseduo-types (e.g. anyarray), which may require additional processing (requiring syscatalog access) to fully resolve to a concrete type.

The built-in function check was in fmgr.h/.c, which defines the Postgres function manager and function-call interface.

bool is_builtin_func(Oid id) { return fmgr_isbuiltin(id) != NULL; }

/*

static const FmgrBuiltin * fmgr_isbuiltin(Oid id) { uint16 index;

/* fast lookup only possible if original oid still assigned */
if (id >= FirstBootstrapObjectId)
    return NULL;

/*
 * Lookup function data. If there's a miss in that range it's likely a
 * nonexistant function, returning NULL here will trigger an ERROR later.
 */
index = fmgr_builtin_oid_index[id];
if (index == InvalidOidBuiltinMapping)
    return NULL;

return &fmgr_builtins[index];

}

The fmgr_builtin_oid_index was populated by the files such as fmgroids.h, fmgrprotos.h, and fmgrtab.c generated by src/backend/utils/Gen_fmgrtab.pl from pg_proc.dat.

johnfangAFW commented 3 years ago

PG supports Create Function to define a custom function

https://www.postgresql.org/docs/11/sql-createfunction.html

Seems YB also supports that

https://docs.yugabyte.com/latest/api/ysql/the-sql-language/statements/ddl_create_function/

Need to find out how the functions including custom ones are evaluated in YB's DocDB.

johnfangAFW commented 3 years ago

The mechanism for YB's Docdb to eval the function is 1) build a c module called ybgate_api to include the expression evaluation related PG code 2) Docdb reconstruct the PG Partial PG expr node or subtree from the serialized string 3) Walk the expr subtree to either eval a value or call the built-in functions based on PG internal logic.

That is to say, YB's Docdb has dependency on a submode of PG, e.g., ybgate_api.

ybgate_api.h // This file includes C/C++ wrappers around some PG utilities such that it can be be included // into YB C++ codebases. // Specifically, it is currently used by DocDB to evaluate YSQL expresison (pushed down from // the query layer).

include "yb/yql/pggate/ybc_pg_typedefs.h"

ifdef __cplusplus

typedef void* YbgExprContext;

else

typedef struct YbgExprContextData* YbgExprContext;

endif

/*

/*

/*

/*

johnfangAFW commented 3 years ago

ybgate_api.c

include "postgres.h"

include

include "ybgate/ybgate_api.h"

include "catalog/pg_type.h"

include "catalog/pg_type_d.h"

include "catalog/ybctype.h"

include "common/int.h"

include "executor/execExpr.h"

include "executor/executor.h"

include "nodes/execnodes.h"

include "nodes/makefuncs.h"

include "nodes/primnodes.h"

include "utils/memutils.h"

include "utils/numeric.h"

struct YbgExprContextData { // Values from table row. int32_t min_attno; int32_t max_attno; Datum attr_vals; Bitmapset attr_nulls; };

The expr eval logic is in the following tree walk logic.

/*

where FunctionCallInvoke was defined in fmgr.h

/*

johnfangAFW commented 3 years ago

Have discussed how Docdb evals the function/expr, let take a look at how the docdb handles the function/expr calls.

doc_expr.cc

case bfpg::TSOpcode::kPgEvalExprCall: {
  const std::string& expr_str = tscall.operands(0).value().string_value();

  std::vector<DocPgParamDesc> params;
  int num_params = (tscall.operands_size() - 1) / 3;
  params.reserve(num_params);
  for (int i = 0; i < num_params; i++) {
    int32_t attno = tscall.operands(3*i + 1).value().int32_value();
    int32_t typid = tscall.operands(3*i + 2).value().int32_value();
    int32_t typmod = tscall.operands(3*i + 3).value().int32_value();
    params.emplace_back(attno, typid, typmod);
  }

  RETURN_NOT_OK(DocPgEvalExpr(expr_str,
                              params,
                              table_row,
                              schema,
                              result));

  return Status::OK();
}

where bfpg::TSOpcode::kPgEvalExprCall maps to PgExpr opcode Opcode::PG_EXPR_EVAL_EXPR_CALL.

docdb_pgapi.cc

Status DocPgEvalExpr(const std::string& expr_str, std::vector params, const QLTableRow& table_row, const Schema schema, QLValue result) { PG_RETURN_NOT_OK(YbgPrepareMemoryContext());

char expr_cstring = const_cast<char >(expr_str.c_str());

// Create the context expression evaluation. // Since we currently only allow referencing the target col just set min/max attr to col_attno. // TODO Eventually this context should be created once per row and contain all (referenced) // column values. Then the context can be reused for all expressions. YbgExprContext expr_ctx; int32_t min_attno = params[0].attno; int32_t max_attno = params[0].attno;

for (int i = 1; i < params.size(); i++) { min_attno = std::min(min_attno, params[i].attno); max_attno = std::max(max_attno, params[i].attno); }

PG_RETURN_NOT_OK(YbgExprContextCreate(min_attno, max_attno, &expr_ctx));

// Set the column values (used to resolve scan variables in the expression). for (const ColumnId& col_id : schema->column_ids()) { auto column = schema->column_by_id(col_id); SCHECK(column.ok(), InternalError, "Invalid Schema");

// Loop here is ok as params.size() will always be 1 for user tables,
// and 2 for some internal queries (catalog version increment).
// TODO Rethink this if we ever allow more params here.
DCHECK_LT(params.size(), 3);
for (int i = 0; i < params.size(); i++) {
  if (column->order() == params[i].attno) {
    const QLValuePB* val = table_row.GetColumn(col_id.rep());
    bool is_null = false;
    uint64_t datum = 0;
    YbgTypeDesc pg_arg_type = {params[i].typid, params[i].typmod};
    const YBCPgTypeEntity *arg_type = DocPgGetTypeEntity(pg_arg_type);
    YBCPgTypeAttrs arg_type_attrs = { pg_arg_type.type_mod };

    Status s = PgValueFromPB(arg_type, arg_type_attrs, *val, &datum, &is_null);
    if (!s.ok()) {
      PG_RETURN_NOT_OK(YbgResetMemoryContext());
      return s;
    }

    PG_RETURN_NOT_OK(YbgExprContextAddColValue(expr_ctx, column->order(), datum, is_null));
    break;
  }
}

}

// Evaluate the expression and get the result. bool is_null = false; uint64_t datum; PG_RETURN_NOT_OK(YbgEvalExpr(expr_cstring, expr_ctx, &datum, &is_null));

// Assuming first arg is the target column, so using it for the return type. // YSQL layer should guarantee this when producing the params. YbgTypeDesc pg_type = {params[0].typid, params[0].typmod}; const YBCPgTypeEntity *ret_type = DocPgGetTypeEntity(pg_type);

Status s = PgValueToPB(ret_type, datum, is_null, result); PG_RETURN_NOT_OK(YbgResetMemoryContext()); return s; }

johnfangAFW commented 3 years ago

where YbgEvalExp was defined in ybgate_api.c

YbgStatus YbgEvalExpr(char expr_cstring, YbgExprContext expr_ctx, uint64_t datum, bool is_null) { PG_SETUP_ERROR_REPORTING(); Expr expr = (Expr ) stringToNode(expr_cstring); datum = (uint64_t) evalExpr(expr_ctx, expr, is_null); return PG_STATUS_OK; }

Obviously, the PG internal Expr node/subtree was deserialized from the expr string, then evalExpr was called to walk the tree to eval the expr node/subtree. The final result, datum, was returned.

johnfangAFW commented 3 years ago

disabled the function pushdown for now by the following PR

https://github.com/futurewei-cloud/chogori-sql/pull/215

and will add support after k2 platform adds such support. https://github.com/futurewei-cloud/chogori-platform/issues/137