DBD-Mimer
view release on metacpan or search on metacpan
*/
char *p = name;
*dest++ = '?';
while(isALNUM(*src)) /* includes '_' */
*p++ = *src++;
*p = 0;
if (DBIc_DEBUGIV(imp_sth) >= 5)
PerlIO_printf(DBIc_LOGPIO(imp_sth), " found named parameter = %s\n",
name);
style = 2;
}
else { /* perhaps ':=' PL/SQL construct */
*dest++ = ch;
continue;
}
*dest = '\0'; /* handy for debugging */
if (laststyle && style != laststyle)
croak("Can't mix placeholder styles (%d/%d)",style,laststyle);
laststyle = style;
if (imp_sth->all_params_hv == NULL)
imp_sth->all_params_hv = newHV();
namelen = strlen(name);
svpp = hv_fetch(imp_sth->all_params_hv, name, namelen, 0);
if (svpp == NULL) {
/* create SV holding the placeholder */
phs_sv = newSVpv((char*)&phs_tpl, sizeof(phs_tpl)+namelen+1);
phs = (phs_t*)SvPVX(phs_sv);
strcpy(phs->name, name);
phs->idx = idx;
/* store placeholder to all_params_hv */
svpp = hv_store(imp_sth->all_params_hv, name, namelen, phs_sv, 0);
}
}
*dest = '\0';
if (imp_sth->all_params_hv) {
DBIc_NUM_PARAMS(imp_sth) = (int)HvKEYS(imp_sth->all_params_hv);
if (DBIc_DEBUGIV(imp_sth) >= 2)
PerlIO_printf(DBIc_LOGPIO(imp_sth), " dbd_preparse scanned %d distinct placeholders\n",
(int)DBIc_NUM_PARAMS(imp_sth));
}
}
int
dbd_st_tables(dbh, sth, catalog, schema, table, table_type)
SV *dbh;
SV *sth;
char *catalog;
char *schema;
char *table;
char *table_type;
{
D_imp_dbh(dbh);
D_imp_sth(sth);
RETCODE rc;
/* SV **svp; */
/* char cname[128]; */ /* cursorname */
dTHR;
imp_sth->henv = imp_dbh->henv; /* needed for dbd_error */
imp_sth->hdbc = imp_dbh->hdbc;
imp_sth->done_desc = 0;
if (!DBIc_ACTIVE(imp_dbh)) {
dbd_error(sth, SQL_ERROR, "Can not allocate statement when disconnected from the database");
return 0;
}
rc = SQLAllocStmt(imp_dbh->hdbc, &imp_sth->hstmt);/* TBD: 3.0 update */
if (rc != SQL_SUCCESS) {
dbd_error(sth, rc, "st_tables/SQLAllocStmt");
return 0;
}
/* just for sanity, later. Any internals that may rely on this (including */
/* debugging) will have valid data */
imp_sth->statement = (char *)safemalloc(strlen(cSqlTables)+
strlen(XXSAFECHAR(catalog)) +
strlen(XXSAFECHAR(schema)) +
strlen(XXSAFECHAR(table)) +
strlen(XXSAFECHAR(table_type))+1);
sprintf(imp_sth->statement, cSqlTables, XXSAFECHAR(catalog),
XXSAFECHAR(schema), XXSAFECHAR(table), XXSAFECHAR(table_type));
rc = SQLTables(imp_sth->hstmt,
(catalog && *catalog) ? catalog : 0, SQL_NTS,
(schema && *schema) ? schema : 0, SQL_NTS,
(table && *table) ? table : 0, SQL_NTS,
table_type && *table_type ? table_type : 0, SQL_NTS /* type (view, table, etc) */
);
if (DBIc_DEBUGIV(imp_sth) >= 2)
PerlIO_printf(DBIc_LOGPIO(imp_dbh), " Tables result %d (%s)\n",
rc, table_type ? table_type : "(null)");
dbd_error(sth, rc, "st_tables/SQLTables");
if (!SQL_ok(rc)) {
SQLFreeHandle(SQL_HANDLE_STMT,imp_sth->hstmt);
/* SQLFreeStmt(imp_sth->hstmt, SQL_DROP);*/ /* TBD: 3.0 update */
imp_sth->hstmt = SQL_NULL_HSTMT;
return 0;
}
return build_results(sth,rc);
}
int
dbd_st_primary_keys(dbh, sth, catalog, schema, table)
SV *dbh;
SV *sth;
char *catalog;
char *schema;
char *table;
{
dTHR;
D_imp_dbh(dbh);
imp_sth->henv = imp_dbh->henv; /* needed for dbd_error */
imp_sth->hdbc = imp_dbh->hdbc;
imp_sth->done_desc = 0;
if (!DBIc_ACTIVE(imp_dbh)) {
dbd_error(sth, SQL_ERROR, "Can not allocate statement when disconnected from the database");
return 0;
}
rc = SQLAllocStmt(imp_dbh->hdbc, &imp_sth->hstmt);/* TBD: 3.0 update */
if (rc != SQL_SUCCESS) {
dbd_error(sth, rc, "odbc_db_primary_key_info/SQLAllocStmt");
return 0;
}
/* just for sanity, later. Any internals that may rely on this (including */
/* debugging) will have valid data */
imp_sth->statement = (char *)safemalloc(strlen(cSqlPrimaryKeys)+
strlen(XXSAFECHAR(catalog))+
strlen(XXSAFECHAR(schema))+
strlen(XXSAFECHAR(table))+1);
sprintf(imp_sth->statement,
cSqlPrimaryKeys, XXSAFECHAR(catalog), XXSAFECHAR(schema),
XXSAFECHAR(table));
rc = SQLPrimaryKeys(imp_sth->hstmt,
(catalog && *catalog) ? catalog : 0, SQL_NTS,
(schema && *schema) ? schema : 0, SQL_NTS,
(table && *table) ? table : 0, SQL_NTS);
if (DBIc_DEBUGIV(imp_sth) >= 2)
PerlIO_printf(DBIc_LOGPIO(imp_dbh), "SQLPrimaryKeys call: cat = %s, schema = %s, table = %s\n",
XXSAFECHAR(catalog), XXSAFECHAR(schema), XXSAFECHAR(table));
dbd_error(sth, rc, "st_primary_key_info/SQLPrimaryKeys");
if (!SQL_ok(rc)) {
SQLFreeHandle(SQL_HANDLE_STMT,imp_sth->hstmt);
/* SQLFreeStmt(imp_sth->hstmt, SQL_DROP);*/ /* TBD: 3.0 update */
imp_sth->hstmt = SQL_NULL_HSTMT;
return 0;
}
return build_results(sth,rc);
}
int
dbd_st_prepare(sth, imp_sth, statement, attribs)
SV *sth;
imp_sth_t *imp_sth;
char *statement;
SV *attribs;
{
dTHR;
D_imp_dbh_from_sth;
RETCODE rc;
/* SV **svp;
char cname[128]; */ /* cursorname */
imp_sth->done_desc = 0;
imp_sth->henv = imp_dbh->henv; /* needed for dbd_error */
imp_sth->hdbc = imp_dbh->hdbc;
imp_sth->odbc_ignore_named_placeholders = imp_dbh->odbc_ignore_named_placeholders;
imp_sth->odbc_default_bind_type = imp_dbh->odbc_default_bind_type;
imp_sth->odbc_force_rebind = imp_dbh->odbc_force_rebind;
if (!DBIc_ACTIVE(imp_dbh)) {
dbd_error(sth, 0, "Can not allocate statement when disconnected from the database");
}
if (!DBIc_ACTIVE(imp_dbh)) {
dbd_error(sth, SQL_ERROR, "Can not allocate statement when disconnected from the database");
return 0;
}
rc = SQLAllocStmt(imp_dbh->hdbc, &imp_sth->hstmt);/* TBD: 3.0 update */
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "st_prepare/SQLAllocStmt");
return 0;
}
imp_sth->odbc_exec_direct = imp_dbh->odbc_exec_direct;
{
/*
* allow setting of odbc_execdirect in prepare() or overriding
*/
SV **odbc_exec_direct_sv;
/* if the attribute is there, let it override what the default
* value from the dbh is (set above).
*/
if ((odbc_exec_direct_sv = DBD_ATTRIB_GET_SVP(attribs, "odbc_execdirect", 10)) != NULL) {
imp_sth->odbc_exec_direct = SvIV(*odbc_exec_direct_sv) != 0;
}
}
/* scan statement for '?', ':1' and/or ':foo' style placeholders */
dbd_preparse(imp_sth, statement);
/* Hold this statement for subsequent call of dbd_execute */
if (!imp_sth->odbc_exec_direct) {
/* parse the (possibly edited) SQL statement */
rc = SQLPrepare(imp_sth->hstmt,
imp_sth->statement, strlen(imp_sth->statement));
if (DBIc_DEBUGIV(imp_sth) >= 2)
PerlIO_printf(DBIc_LOGPIO(imp_dbh), " SQLPrepare returned %d\n\n",
rc);
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "st_prepare/SQLPrepare");
SQLFreeHandle(SQL_HANDLE_STMT,imp_sth->hstmt);
/* SQLFreeStmt(imp_sth->hstmt, SQL_DROP);*/ /* TBD: 3.0 update */
imp_sth->hstmt = SQL_NULL_HSTMT;
return 0;
}
}
if (DBIc_DEBUGIV(imp_sth) >= 2)
PerlIO_printf(DBIc_LOGPIO(imp_dbh), " dbd_st_prepare'd sql f%d, ExecDirect=%d\n\t%s\n",
imp_sth->hstmt, imp_sth->odbc_exec_direct, imp_sth->statement);
* Since we've detected the problem locally via the datalen,
* we don't need to worry about the value of rc.
*
* This used to make sure rc was set to SQL_SUCCESS_WITH_INFO
* but since it's an error and not SUCCESS, call dbd_error()
* with SQL_ERROR explicitly instead.
*/
dbd_error(sth, SQL_ERROR, "st_fetch/SQLFetch (long truncated DBI attribute LongTruncOk not set and/or LongReadLen too small)");
return Nullav;
}
/* LongTruncOk true, just ensure perl has the right length
* for the truncated data.
*/
sv_setpvn(sv, (char*)fbh->data, fbh->ColDisplaySize);
}
else switch(fbh->ftype) {
#ifdef TIMESTAMP_STRUCT /* iODBC doesn't define this */
TIMESTAMP_STRUCT *ts;
case SQL_C_TIMESTAMP:
case SQL_C_TYPE_TIMESTAMP:
ts = (TIMESTAMP_STRUCT *)fbh->data;
sprintf(cvbuf, "%04d-%02d-%02d %02d:%02d:%02d",
ts->year, ts->month, ts->day,
ts->hour, ts->minute, ts->second, ts->fraction);
sv_setpv(sv, cvbuf);
break;
#endif
default:
if (ChopBlanks && fbh->ColSqlType == SQL_CHAR && fbh->datalen > 0) {
char *p = (char*)fbh->data;
while(fbh->datalen && p[fbh->datalen - 1]==' ')
--fbh->datalen;
}
sv_setpvn(sv, (char*)fbh->data, fbh->datalen);
}
}
return av;
}
int
dbd_st_rows(sth, imp_sth)
SV *sth;
imp_sth_t *imp_sth;
{
return imp_sth->RowCount;
}
int
dbd_st_finish(sth, imp_sth)
SV *sth;
imp_sth_t *imp_sth;
{
dTHR;
D_imp_dbh_from_sth;
RETCODE rc;
int ret = 0;
/* Cancel further fetches from this cursor. */
/* We don't close the cursor till DESTROY (dbd_st_destroy). */
/* The application may re execute(...) it. */
/* XXX semantics of finish (eg oracle vs odbc) need lots more thought */
/* re-read latest DBI specs and ODBC manuals */
if (DBIc_ACTIVE(imp_sth) && imp_dbh->hdbc != SQL_NULL_HDBC) {
rc = SQLFreeStmt(imp_sth->hstmt, SQL_CLOSE);/* TBD: 3.0 update */
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "finish/SQLFreeStmt(SQL_CLOSE)");
return 0;
}
if (DBIc_DEBUGIV(imp_sth) > 5) {
PerlIO_printf(DBIc_LOGPIO(imp_dbh), "dbd_st_finish closed query:\n");
}
}
DBIc_ACTIVE_off(imp_sth);
return 1;
}
void
dbd_st_destroy(sth, imp_sth)
SV *sth;
imp_sth_t *imp_sth;
{
dTHR;
D_imp_dbh_from_sth;
RETCODE rc;
/* Free contents of imp_sth */
/* PerlIO_printf(DBIc_LOGPIO(imp_dbh), " dbd_st_destroy\n"); */
Safefree(imp_sth->fbh);
Safefree(imp_sth->RowBuffer);
Safefree(imp_sth->ColNames);
Safefree(imp_sth->statement);
if (imp_sth->out_params_av)
sv_free((SV*)imp_sth->out_params_av);
if (imp_sth->all_params_hv) {
HV *hv = imp_sth->all_params_hv;
SV *sv;
char *key;
I32 retlen;
hv_iterinit(hv);
while( (sv = hv_iternextsv(hv, &key, &retlen)) != NULL ) {
if (sv != &sv_undef) {
phs_t *phs_tpl = (phs_t*)(void*)SvPVX(sv);
sv_free(phs_tpl->sv);
}
}
sv_free((SV*)imp_sth->all_params_hv);
}
/* SQLxxx functions dump core when no connection exists. This happens
* when the db was disconnected before perl ending. Hence,
* checking for the dirty flag.
*/
if (imp_dbh->hdbc != SQL_NULL_HDBC && !dirty) {
* the result set to operate, hence don't do a describe unless you need
* to do one.
* DBD::ODBC 0.45_15
* */
typedef struct {
const char *str;
unsigned len:8;
unsigned array:1;
unsigned need_describe:1;
unsigned filler:22;
} T_st_params;
#define s_A(str,need_describe) { str, sizeof(str)-1,0,need_describe }
static T_st_params S_st_fetch_params[] =
{
s_A("NUM_OF_PARAMS",1), /* 0 */
s_A("NUM_OF_FIELDS",1), /* 1 */
s_A("NAME",1), /* 2 */
s_A("NULLABLE",1), /* 3 */
s_A("TYPE",1), /* 4 */
s_A("PRECISION",1), /* 5 */
s_A("SCALE",1), /* 6 */
s_A("sol_type",1), /* 7 */
s_A("sol_length",1), /* 8 */
s_A("CursorName",1), /* 9 */
s_A("odbc_more_results",1), /* 10 */
s_A("ParamValues",1), /* 11 */
s_A("LongReadLen",0), /* 12 */
s_A("odbc_ignore_named_placeholders",0), /* 13 */
s_A("odbc_default_bind_type",0), /* 14 */
s_A("odbc_force_rebind",0), /* 15 */
s_A("",0), /* END */
};
static T_st_params S_st_store_params[] =
{
s_A("odbc_ignore_named_placeholders",0), /* 0 */
s_A("odbc_default_bind_type",0), /* 1 */
s_A("odbc_force_rebind",0), /* 2 */
s_A("",0), /* END */
};
#undef s_A
/*----------------------------------------
* dummy routines st_XXXX
*----------------------------------------
*/
SV *
dbd_st_FETCH_attrib(sth, imp_sth, keysv)
SV *sth;
imp_sth_t *imp_sth;
SV *keysv;
{
dTHR;
STRLEN kl;
char *key = SvPV(keysv,kl);
int i;
SV *retsv = NULL;
T_st_params *par;
char cursor_name[256];
SWORD cursor_name_len;
RETCODE rc;
for (par = S_st_fetch_params; par->len > 0; par++)
if (par->len == kl && strEQ(key, par->str))
break;
if (par->len <= 0)
return Nullsv;
if (par->need_describe && !imp_sth->done_desc && !dbd_describe(sth, imp_sth))
{
/* dbd_describe has already called dbd_error() */
/* we can't return Nullsv here because the xs code will */
/* then just pass the attribute name to DBI for FETCH. */
croak("Describe failed during %s->FETCH(%s)",
SvPV(sth,na), key);
}
i = DBIc_NUM_FIELDS(imp_sth);
switch(par - S_st_fetch_params)
{
AV *av;
case 0: /* NUM_OF_PARAMS */
return Nullsv; /* handled by DBI */
case 1: /* NUM_OF_FIELDS */
if (DBIc_DEBUGIV(imp_sth) > 8) {
PerlIO_printf(DBIc_LOGPIO(imp_sth), " dbd_st_FETCH_attrib NUM_OF_FIELDS %d\n", i);
}
retsv = newSViv(i);
break;
case 2: /* NAME */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
if (DBIc_DEBUGIV(imp_sth) > 8) {
int j;
PerlIO_printf(DBIc_LOGPIO(imp_sth), " dbd_st_FETCH_attrib NAMES %d\n", i);
for (j = 0; j < i; j++) {
PerlIO_printf(DBIc_LOGPIO(imp_sth), "\t%s\n", imp_sth->fbh[j].ColName);
PerlIO_flush(DBIc_LOGPIO(imp_sth));
}
PerlIO_flush(DBIc_LOGPIO(imp_sth));
}
while(--i >= 0) {
if (DBIc_DEBUGIV(imp_sth) > 8) {
PerlIO_printf(DBIc_LOGPIO(imp_sth), " Colname %d => %s\n",
i, imp_sth->fbh[i].ColName);
PerlIO_flush(DBIc_LOGPIO(imp_sth));
}
av_store(av, i, newSVpv(imp_sth->fbh[i].ColName, 0));
}
break;
case 3: /* NULLABLE */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i,
(imp_sth->fbh[i].ColNullable == SQL_NO_NULLS)
? &sv_no : &sv_yes);
break;
case 4: /* TYPE */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i, newSViv(imp_sth->fbh[i].ColSqlType));
break;
case 5: /* PRECISION */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i, newSViv(imp_sth->fbh[i].ColDef));
break;
case 6: /* SCALE */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i, newSViv(imp_sth->fbh[i].ColScale));
break;
case 7: /* sol_type */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i, newSViv(imp_sth->fbh[i].ColSqlType));
break;
case 8: /* sol_length */
av = newAV();
retsv = newRV(sv_2mortal((SV*)av));
while(--i >= 0)
av_store(av, i, newSViv(imp_sth->fbh[i].ColLength));
break;
case 9: /* CursorName */
rc = SQLGetCursorName(imp_sth->hstmt,
cursor_name, sizeof(cursor_name), &cursor_name_len);
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "st_FETCH/SQLGetCursorName");
return Nullsv;
}
retsv = newSVpv(cursor_name, cursor_name_len);
break;
case 10: /* odbc_more_results */
retsv = newSViv(imp_sth->moreResults);
break;
case 11:
{
/* not sure if there's a memory leak here. */
HV *paramvalues = newHV();
if (imp_sth->all_params_hv) {
HV *hv = imp_sth->all_params_hv;
SV *sv;
char *key;
I32 retlen;
hv_iterinit(hv);
while( (sv = hv_iternextsv(hv, &key, &retlen)) != NULL ) {
if (sv != &sv_undef) {
phs_t *phs = (phs_t*)(void*)SvPVX(sv);
hv_store(paramvalues, phs->name, strlen(phs->name), newSVsv(phs->sv), 0);
}
}
}
/* ensure HV is freed when the ref is freed */
retsv = newRV_noinc((SV *)paramvalues);
}
break;
case 12:
retsv = newSViv(DBIc_LongReadLen(imp_sth));
break;
case 13:
retsv = newSViv(imp_sth->odbc_ignore_named_placeholders);
break;
case 14:
retsv = newSViv(imp_sth->odbc_default_bind_type);
break;
case 15: /* force rebind */
retsv = newSViv(imp_sth->odbc_force_rebind);
break;
default:
return Nullsv;
}
return sv_2mortal(retsv);
}
int
dbd_st_STORE_attrib(sth, imp_sth, keysv, valuesv)
SV *sth;
imp_sth_t *imp_sth;
SV *keysv;
SV *valuesv;
{
dTHR;
D_imp_dbh_from_sth;
STRLEN kl;
STRLEN vl;
char *key = SvPV(keysv,kl);
char *value = SvPV(valuesv, vl);
T_st_params *par;
rc = SQLAllocStmt(imp_dbh->hdbc, &imp_sth->hstmt);/* TBD: 3.0 update */
if (rc != SQL_SUCCESS) {
dbd_error(sth, rc, "odbc_get_foreign_keys/SQLAllocStmt");
return 0;
}
rc = SQLForeignKeys(imp_sth->hstmt,
PK_CatalogName, strlen(PK_CatalogName),
PK_SchemaName, strlen(PK_SchemaName),
PK_TableName, strlen(PK_TableName),
FK_CatalogName, strlen(FK_CatalogName),
FK_SchemaName, strlen(FK_SchemaName),
FK_TableName, strlen(FK_TableName));
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "odbc_get_foreign_keys/SQLForeignKeys");
return 0;
}
return build_results(sth,rc);
}
int
odbc_describe_col(sth, colno, ColumnName, BufferLength, NameLength, DataType, ColumnSize, DecimalDigits, Nullable)
SV *sth;
int colno;
char *ColumnName;
I16 BufferLength;
I16 *NameLength;
I16 *DataType;
U32 *ColumnSize;
I16 *DecimalDigits;
I16 *Nullable;
{
D_imp_sth(sth);
SQLUINTEGER ColSize;
RETCODE rc;
rc = SQLDescribeCol(imp_sth->hstmt, colno,
ColumnName, BufferLength, NameLength,
DataType, &ColSize, DecimalDigits, Nullable);
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "DescribeCol/SQLDescribeCol");
return 0;
}
*ColumnSize = ColSize;
return 1;
}
int
odbc_get_type_info(dbh, sth, ftype)
SV *dbh;
SV *sth;
int ftype;
{
dTHR;
D_imp_dbh(dbh);
D_imp_sth(sth);
RETCODE rc;
#if 0
/* TBD: cursorname? */
char cname[128]; /* cursorname */
#endif
imp_sth->henv = imp_dbh->henv; /* needed for dbd_error */
imp_sth->hdbc = imp_dbh->hdbc;
imp_sth->done_desc = 0;
if (!DBIc_ACTIVE(imp_dbh)) {
dbd_error(sth, SQL_ERROR, "Can not allocate statement when disconnected from the database");
return 0;
}
rc = SQLAllocStmt(imp_dbh->hdbc, &imp_sth->hstmt);/* TBD: 3.0 update */
if (rc != SQL_SUCCESS) {
dbd_error(sth, rc, "odbc_get_type_info/SQLGetTypeInfo");
return 0;
}
/* just for sanity, later. Any internals that may rely on this (including */
/* debugging) will have valid data */
imp_sth->statement = (char *)safemalloc(strlen(cSqlGetTypeInfo)+ftype/10+1);
sprintf(imp_sth->statement, cSqlGetTypeInfo, ftype);
rc = SQLGetTypeInfo(imp_sth->hstmt, ftype);
dbd_error(sth, rc, "odbc_get_type_info/SQLGetTypeInfo");
if (!SQL_ok(rc)) {
SQLFreeHandle(SQL_HANDLE_STMT,imp_sth->hstmt);
/* SQLFreeStmt(imp_sth->hstmt, SQL_DROP);*/ /* TBD: 3.0 update */
imp_sth->hstmt = SQL_NULL_HSTMT;
return 0;
}
return build_results(sth,rc);
}
SV *
odbc_cancel(sth)
SV *sth;
{
dTHR;
D_imp_sth(sth);
RETCODE rc;
if ( !DBIc_ACTIVE(imp_sth) ) {
dbd_error(sth, SQL_ERROR, "no statement executing");
return Nullsv;
}
rc = SQLCancel(imp_sth->hstmt);
if (!SQL_ok(rc)) {
dbd_error(sth, rc, "odbc_cancel/SQLCancel");
return Nullsv;
}
return newSViv(1);
}
SV *
( run in 0.647 second using v1.01-cache-2.11-cpan-39bf76dae61 )