class ForeignServer
{
public:
explicit ForeignServer(MemoryPool& p, const MetaName& aName, const MetaName& aPlugin)
: name(p, aName), plugin(p, aPlugin), options(p)
{}
...
private:
const MetaName name;
const MetaName plugin;
Firebird::GenericMap<MetaNameStringPair> options;
};
class ForeignTableAdapter
{
public:
class ForeignField
{
...
MetaName name;
Firebird::GenericMap<Firebird::Full<MetaName, Firebird::string> > options;
};
...
jrd_rel* m_relation;
Firebird::AutoPtr<ForeignServer> m_server;
ForeignTableProvider* m_provider = NULL;
ForeignTableConnection* m_connection = NULL;
Firebird::GenericMap<MetaNameStringPair> m_tableOptions;
Firebird::LeftPooledMap<MetaName, ForeignField*> m_foreignFields;
...
};
class jrd_rel : public pool_alloc<type_rel>
{
…
ForeignTableAdapter* rel_foreign_adapter;
…
}
class ForeignTableConnection : public EDS::IscConnection
{
void getProviderInfo(thread_db* tdbb);
bool testSqlFeature(info_sql_features value) const { return m_sqlFeatures[value]; }
};
class ForeignTableProvider : public EDS::IscProvider
{
ForeignTableConnection* createForeignConnection(thread_db* tdbb, ForeignServer* server);
};
class ForeignTableStatement : public EDS::IscStatement
{
void setSql(const Firebird::string& sql);
void openInternal(thread_db* tdbb, EDS::IscTransaction* transaction);
void executeInternal(thread_db* tdbb, EDS::IscTransaction* transaction, record_param* org_rpb, record_param* new_rpb, const Firebird::Array<int>* skippedParameters = nullptr);
bool fetchInternal(thread_db* tdbb, Record* record);
};
The new record stream performs foreign data fetching:
class ForeignTableScan final : public RecordStream // src/jrd/recsrc/RecordSource.h
{
private:
struct Impure : public RecordSource::Impure
{
EDS::Statement* statement;
};
public:
ForeignTableScan(CompilerScratch* csb, const Firebird::string& alias, StreamType stream, jrd_rel* relation);
...
private:
...
jrd_rel* const m_relation;
const Firebird::string m_alias;
Firebird::Array<const BoolExprNode*> m_filterNodes; // Stores nodes will be converted to a condition
const SortNode* m_sortNode; // Stores nodes will be converted to order by
};
3.2.3. Data modification
else if (relation->rel_foreign_adapter)
{
if (!impure->statement)
{
impure->statement = relation->rel_foreign_adapter->createStatement(tdbb, ...);
impure->statement->bindToRequest(request, &impure->statement);
}
relation->rel_foreign_adapter->execute(tdbb, impure->statement, NULL, rpb); // Store. src/dsql/StmtNodes.cpp
Or for UPDATE:relation->rel_foreign_adapter->execute(tdbb, impure->statement, NULL, rpb); // Erase. src/dsql/StmtNodes.cpp
}
// src/jrd/exe.cpp
// Close external statements
while (request->req_ext_stmt)
request->req_ext_stmt->close(tdbb);create database '172.17.0.2:/db/external.fdb' user SYSDBA password 'masterkey';
connect '172.17.0.2:/db/external.fdb' user SYSDBA password 'masterkey';
create table ft_test(
f_id bigint,
f_vchar varchar(10),
f_blob blob sub_type binary,
f_bool boolean);
commit;
create user ext_user password 'ext_password';
grant all on ft_test to ext_user;
commit;
connect 'localhost:/db/local.fdb' user SYSDBA password 'masterkey';
create server test_server foreign data wrapper "Remote" options(connection_string '172.17.0.2:/db/external.fdb', user 'ext_user', password 'ext_password');
create foreign table test(id bigint options(column_name 'f_id'),
vchar varchar(10) options(column_name = 'f_vchar'),
fblob blob sub_type binary options(column_name = 'f_blob'),
fbool boolean options(column_name = 'f_bool')
)
server test_server options(table_name 'ft_test');
commit;
select id, vchar, cast(fblob as varchar(20)), fbool from test rows 5;
ID VCHAR CAST FBOOL
===================== ========== ==================== =======
11 htiVSHUpUa '.a....k.. <true>
12 jOESzIJDvn Y.z.....tl.F........ <false>
13 sBv2zl76sV D9.D.8 >..J..]
. <false>
14 O6d2RWV2tG 5....m..A. <true>
15 bYuyWGUrjI #D.......7...=.xy. <false>
alter server test_server options(drop user, drop password);
commit;
select id, vchar, cast(fblob as varchar(20)), fbool from test rows 5;
ID VCHAR CAST FBOOL
===================== ========== ==================== =======
Statement failed, SQLSTATE = 42000
Execute statement error at attach :
335545106 : Error occurred during login, please check server rdbserver.log for details
Data source : Firebird::172.17.0.2:/db/external.fdb
create user mapping for sysdba
server test_server
options (user 'ext_user', password 'ext_password');
commit;
select id, vchar, cast(fblob as varchar(20)), fbool from test rows 5;
ID VCHAR CAST FBOOL
===================== ========== ==================== =======
11 htiVSHUpUa '.a....k.. <true>
12 jOESzIJDvn Y.z.....tl.F........ <false>
13 sBv2zl76sV D9.D.8 >..J..]
. <false>
14 O6d2RWV2tG 5....m..A. <true>
15 bYuyWGUrjI #D.......7...=.xy. <false>
alter user mapping for sysdba
server test_server
options (drop password);
commit;
select id, vchar, cast(fblob as varchar(20)), fbool from test rows 5;
ID VCHAR CAST FBOOL
===================== ========== ==================== =======
Statement failed, SQLSTATE = 42000
Execute statement error at attach :
335544472 : Your user name and password are not defined. Ask your database administrator to set up a Red Database login.
Data source : Firebird::172.17.0.2:/db/external.fdb
execute block returns (ext_id bigint, ext_vchar varchar(10), ext_blob varchar(20), ext_bool boolean) as
begin
for execute statement
'select f_id, f_vchar, f_blob, f_bool from ft_test'
on external server test_server password 'ext_password'
into :ext_id, :ext_vchar, :ext_blob, :ext_bool DO
suspend;
end^
EXT_ID EXT_VCHAR EXT_BLOB EXT_BOOL
===================== ========== ==================== ========
11 htiVSHUpUa '.a....k.. <true>
12 jOESzIJDvn Y.z.....tl.F........ <false>
13 sBv2zl76sV D9.D.8 >..J..]
. <false>
14 O6d2RWV2tG 5....m..A. <true>
15 bYuyWGUrjI #D.......7...=.xy. <false>
16 <null> 8....s...V. <true>
17 mU3TNIXXU9 .......[8%$....R <true>
18 vfRdSpZP1s ..h..."[E0RbXT..c.. <true>
19 5VmDx2E0IK ...c3{...?}.:.... <false>
20 TiEftYzQpF ...+..
... <false>
21 3ebLTJqnTZ .h .$..... <true>
Vasiliy Yashkov wrote 21.01.2025 9:53:
> In Firebird, the foreign-data wrapper can be implemented as a provider (plugin).
> To use a foreign-data wrapper, it should be defined in plugins.conf, for example:
Why? Usually plugins don't need that.
> A foreign table is a table that is physically stored on a foreign server but is accessible by the local DBMS. The foreign table allows executing SQL queries to the data contained in the table as local ones. The foreign table includes the table structure description and column mapping between the foreign and local table.
What's the purpose of it? You can get whole table definition from foreign server.
> RDB$FOREIGN_SERVER_OPTIONS - contains a description of all options defined for the foreign servers in the database:
Usually such things are stored in BLOB field.
> 3.2. Code highlights
I see that whole implementation is built-in and uses internal engine
structures, so what meaning does plugin configuration above have?
> The optimizer extracts local predicates to the foreign table and passes them to the `Jrd::RecordSource` object (ForeignTableScan), which analyzes them and converts them into a `WHERE` clause for a query to the foreign server.
Bad idea. Generating of SQL (if necessary) is the job for FDW, not optimizer
because optimize lack of knowledge of target database's SQL dialect. You also
are loosing ability to work with data sources not supporting SQL.
> It was not possible to test the insertion with the Magpie provider, because the ODBC driver does not support the Unicode API and requires more long term development.
What do you mean? Which ODBC driver and foreign server you used? I would
suggest MySQL as the second most popular DBMS. And its drivers support Unicode
API well.
Am I right in thinking that it is enough to specify the provider in `USING PLUGIN` and not specify it in the Providers configuration parameter (firebird.conf)? If so, that's great.
Not bad for the initial implementation. True, I did not quite understand the meaning of what GENERATED ALWAYS AS IDENTITY gives? Let's say some restrictions can be checked on the Firebird side, but it is not very clear how the identity columns are used?
By the way, I remembered another problem with ODBCEngine and executing queries to it via EXECUTE STATEMENT. Not all drivers report the types of input parameters. However, our prepare prepares input and output messages. Then we can adjust them during execution, but only adjust them. For example, MySQL and its ODBC driver do not report the types of input parameters, but only their number. In this case, the ideal option would be for such drivers to simply pass the types of input parameters from the outside, that is, what is passed to EXECUTE STATEMENT. Has this problem been solved somehow?
What do you mean? I see in code that it is quite possible to derive needed
types from EDS parameter's definition and pass these types to the statement's
InputMetadata.
I mean the current implementation requires the provider to return an input message. If some driver does not return parameter types, then in this case the input message will be incomplete, and you can get an internal error (typically in CVT_move_common).
But EDS knows the types of input parameters in the place where we call it. It would be possible to pass an additional option from the provider, for example in isc_feature, that the provider cannot provide full information about the types of input parameters, and then simply ignore the InputMetadata that is returned from it and replace it with information about the EDS parameters.