Aller directement au contenu principal

Comment implémenter des fonctions scalaires SQL personnalisées dans un pilote ODBC

Insight Software

insightsoftware est le fournisseur le plus complet de solutions destinées au service du directeur financier. Nous transformons les informations en connaissances, permettant ainsi aux dirigeants d'entreprise de piloter leur organisation de manière stratégique.

Comment implémenter des fonctions scalaires SQL personnalisées dans un pilote ODBC

Le SDK SimbaEngine™ vous permet de créer un pilote ODBC personnalisé grâce auquel vous pouvez connecter votre source de données à n'importe quelle application ODBC. Le moteur SQLEngine intégré au SDK SimbaEngine est flexible et vous permet d'implémenter vos propres fonctions scalaires et d'agrégation personnalisées pour votre source de données. Dans de nombreuses nouvelles sources de données, de nouvelles fonctionnalités sont rendues possibles par des fonctions scalaires qui ne figurent pas dans la spécification SQL standard. Par exemple, une nouvelle base de données en mémoire peut calculer la moyenne de N colonnes sur un ensemble de N tables de manière extrêmement efficace en appelant une fonction scalaire. Sans la possibilité d’exposer cette fonctionnalité, vos clients ne pourraient pas tirer pleinement parti de votre source de données.

Dans cet article, je vais vous présenter un exemple simple d'implémentation d'une fonction scalaire personnalisée dans l'exemple « Quickstart » du SDK SimbaEngine. Cette fonction scalaire personnalisée servira à vérifier si une chaîne de caractères contient le caractère « e », sans distinction de majuscules/minuscules. Elle ne prendra qu'un seul argument, à savoir une valeur de type caractère, et vérifiera si celui-ci contient le caractère « e ». Elle renverra ensuite un SQL_BIT qui représente le Booléen valeur de la fonction scalaire. Par exemple, une requête SQL utilisant notre fonction scalaire personnalisée peut se présenter comme suit, et cette requête renverra toutes les lignes où NOM DE FAMILLE contient un « e ». Le code d'exemple correspondant à cet exemple est disponible au bas de la page et peut être modifié pour répondre à vos besoins.

Mais comment allez-vous exécuter votre fonction scalaire ? Il existe deux options pour exécuter une fonction scalaire dans un pilote ODBC : l’une consiste à la faire exécuter par le pilote, et l’autre à laisser votre source de données s’en charger. Votre choix dépend de ce que fait votre fonction scalaire. Par exemple, si votre fonction scalaire est une fonction complexe pouvant être exécutée facilement et efficacement au sein de votre source de données, il serait préférable de laisser la base de données s’en charger plutôt que de l’implémenter au sein du pilote. Si vous vous connectez à une base de données via un réseau et que vous souhaitez réduire le nombre d’appels vers la base de données, il peut être préférable d’implémenter votre fonction scalaire dans votre pilote. La manière dont vous implémenterez une fonction scalaire personnalisée dépendra de vos besoins métier.

Une fois que vous avez déterminé comment vous allez implémenter votre fonction scalaire, vous implémentez une classe dérivée de

    1. Créez une classe nommée QSScalarFnContainsE qui hérite de DSIExtScalarFunction et implémenter chaque fonction virtuelle. Le constructeur de QSScalarFnContainsE prend un pointeur vers le Objet ILogger  SimbaEngine Logging (document Word), ce qui permet de consigner toutes les actions effectuées dans la classe à des fins de débogage. Outre l’objet de journalisation, un pointeur vers la structure de paramètres du pilote est transmis ; celle-ci sert à définir certaines métadonnées. Si vous disposiez d’une API ou d’un autre objet permettant de communiquer avec votre source de données, vous pourriez le transmettre au constructeur ou l’intégrer dans la structure de paramètres du pilote. Dans le constructeur, vous créerez les métadonnées de colonne pour l’entrée et la sortie de la fonction scalaire. Ces métadonnées de colonne indiqueront à notre SQLEngine et à toute autre application à quoi s’attendre lors de l’utilisation de cette fonction scalaire, notamment le type que doivent avoir l’entrée et la sortie. Cette fonction scalaire personnalisée prendra 1 entrée de type caractère et aura 1 sortie qui sera un SQL_BIT pour indiquer le Booléen valeur de la fonction. Des accesseurs simples sont utilisés pour les métadonnées d'entrée et de sortie, ainsi que pour le nom de la fonction scalaire personnalisée.

      QSScalarFnContainsE::QSScalarFnContainsE(ILogger* in_logger, QuickstartSettings* in_settings) : DSIExtScalarFunction(), m_logger(in_logger), m_settings(in_settings), m_result(false) {m_logger->LogFunctionEntrance( "Simba::Quickstart", "QSScalarFnContainsE", "QSScalarFnContainsE");// Create Output metadata. m_outputMeta = SqlTypeMetadataFactorySingleton::GetInstance() ->CreateNewSqlTypeMetadata(SQL_BIT);// Create Input Metadata. m_inputMeta.push_back( SqlTypeMetadataFactorySingleton::GetInstance() ->CreateNewSqlTypeMetadata(SQL_WVARCHAR));m_inputMeta.back() -> SetLengthOrIntervalPrecision(m_settings -> defaultMaxColumnSize) ; }

    2. Dans la fonction UpdateMetadata(), vous mettrez à jour les métadonnées d'entrée au moment de l'exécution de la fonction scalaire afin de garantir que nous disposions des métadonnées correctes pendant l'exécution. Dans cette fonction scalaire, vous ne mettrez à jour les métadonnées qu'au moment de l'exécution et non pendant la phase de préparation, car les métadonnées de la phase de préparation peuvent être invalides. Par exemple, si votre requête contient un paramètre parmi les arguments de la fonction scalaire, vous n'avez pas besoin de spécifier le type de ce paramètre avant l'exécution. Lorsque vous appelez SQLPrepare Dans votre requête SQL, les métadonnées relatives à ce paramètre pourraient indiquer qu'il s'agit d'un SQL_INTEGER type, car vous n'avez pas précisé le type de votre paramètre. Lorsque vous liez un paramètre, vous pouvez indiquer que son type est SQL_WVARCHAR puis exécutez la requête SQL. Dans UpdateMetadata(), vous vérifierez d'abord que les métadonnées d'entrée sont de type caractère, puis vous mettrez à jour la précision de ce type. Pour un type caractère, la précision correspond à la taille des données de caractère. Si les métadonnées d'entrée ne sont pas de type caractère, une exception sera levée indiquant que l'entrée n'est pas valide, car cette fonction scalaire ne prend pas en charge les arguments qui ne sont pas de type caractère.

      bool QSScalarFnContainsE::UpdateMetadata( const std::vector<SqlTypeMetadata*>& in_inputMetadata, bool in_isPrepare) { m_logger->LogFunctionEntrance( "Simba::Quickstart", "QSScalarFnContainsE", "UpdateMetadata");if (!in_isPrepare) { // Only allow character types. if (!in_inputMetadata[0]->IsAnyCharacterType()) { QSTHROWGEN2( L"QSInvalidScalarFnParameterType", "1", in_inputMetadata[0]->GetTypeName()); }// Update the lengths m_inputMeta[0]->SetLengthOrIntervalPrecision( in_inputMetadata[0]->GetLengthOrIntervalPrecision());return true; }// Metadata will not change at prepare time. return false; }

    3. Le Exécuter() La fonction contient la logique proprement dite de la fonction scalaire. Dans cet exemple, notre fonction scalaire vérifiera si la chaîne fournie en entrée contient un caractère « e » et stockera ce résultat dans une variable membre de la classe en vue d’une utilisation ultérieure. Le moteur SQL appellera RetrieveData() après Exécuter() pour obtenir le résultat de l'exécution de la fonction scalaire ; notre fonction scalaire personnalisée renverra ce résultat sous la forme SQL_BIT en utilisant notre variable membre.

      void QSScalarFnContainsE::Execute( Simba::SQLEngine::InputValues& in_inputValues) { m_logger->LogFunctionEntrance( "Simba::Quickstart", "QSScalarFnContainsE", "Execute");simba_wstring value1 ; in_inputValues[0].GetWideStringValue(value1) ; value1 = value1.ToLower() ;// Store result as a local variable. m_result = SIMBA_NPOS != value1.Find(L"e"); } bool QSScalarFnContainsE::RetrieveData( SqlData* io_data, simba_signed_native in_offset, simba_signed_native in_maxSize) { // Convert m_result in to a SQL_BIT. *reinterpret_cast<simba_byte*>(io_data ->GetBuffer()) = m_result ? 1 : 0; return false; }

    4. Pour que Simba SQLEngine puisse utiliser la fonction scalaire personnalisée, vous devez implémenter une méthode dans la classe du moteur de données nommée OpenScalarFunction(). Dans cette fonction, vous vérifierez que la fonction scalaire appelée est « ContainsE » et que le nombre d'arguments est égal à un. Si la fonction scalaire personnalisée répond à toutes les exigences, vous instanciez alors la classe de la fonction scalaire personnalisée et la renvoyez au moteur SQL (SQLEngine) pour qu’il puisse l’utiliser. Si le nombre d’arguments n’est pas égal à un, vous lancez une exception indiquant que le nombre d’arguments est incorrect. S’il n’existe aucune fonction scalaire personnalisée portant le nom spécifié, vous renvoyez alors NULL pour indiquer qu’il n’y a pas de fonction scalaire personnalisée correspondante.

      SharedPtr<DSIExtScalarFunction> QSDataEngine::OpenScalarFunction( const simba_wstring& in_scalarName, simba_size_t in_numArguments) { ENTRANCE_LOG( GetLog(), "Simba::Quickstart", "QSDataEngine", "OpenScalarFunction");// Check to see the name of the scalar function is ContainsE. if (in_scalarName.IsEqual(QS_CONTAINSE_NAME, false)) { if (in_numArguments != 1) { // Throw exception to SQL Engine if argument count is off. SETHROW_SQL_ERR1( SE_ERR_INVALID_SCALAR_FN_ARG_COUNT, QS_CONTAINSE_NAME); }// Return instance of custom scalar function class. return SharedPtr<DSIExtScalarFunction> (new QSScalarFnContainsE(GetLog(), m_settings)); }// If scalar function does not exist then we return null. return SharedPtr<DSIExtScalarFunction>(NULL); } Lors de la compilation du pilote, vous pouvez obtenir un message d'erreur indiquant qu'il n'a pas été possible d'inclure le fichier qui fait partie de votre nouvelle fonction scalaire. Les fonctions scalaires personnalisées étant des fonctionnalités atypiques des pilotes, nous ajoutons des chemins d'accès supplémentaires pour compiler le pilote. Dans notre exemple, récupérez un fichier situé dans le répertoire ETree module du SDK SimbaEngine.

  • Faites un clic droit sur votre projet de pilote, puis cliquez sur Propriétés. Cela ouvrira la page « Propriétés » de votre projet Visual Studio. Sur le côté gauche, vous devriez voir un menu déroulant pour Propriétés de configuration, développez l'onglet, puis développez le C/C++ sous-onglet. Dans l'onglet C/C++, sélectionnez le Généralités section et votre fenêtre principale devrait afficher les paramètres généraux « C/C++ ». Ajoutez le répertoire d'inclusion supplémentaire dans le Répertoires d'inclusion supplémentaires section. Vous pouvez utiliser le $(SIMBAENGINE_DIR) variable d'environnement :

    $(SIMBAENGINE_DIR)\Include\SQLEngine\Executor\ETree

    Comment implémenter du SQL personnalisé

    Fig. 1 : Boîte de dialogue « Répertoires d'inclusion supplémentaires » dans Visual Studio 2022. Pour un pilote qui sera compilé sous Linux, vous devrez modifier Makefile_SRCS.mak et Makefile_FLAGS.mak pour inclure les fichiers récemment ajoutés. Makefile_SRCS.mak et Makefile_FLAGS.mak sera dans le Source répertoire du projet du pilote. Pour Makefile_SRCS.mak, ajoutez l'emplacement du fichier QSScalarFNContainsE.cpp dans le Section COMMON_SRCS. Si vous avez placé le fichier dans DataEngine dossier, puis vous ajouteriez DataEngine/QSScalarFnContainsE.cpp dans la liste COMMON_SRCS. Pour le fichier Makefile_FLAGS.mak, ajoutez dans le répertoire d'inclusion du ETree sous le COMMON_CFLAGS section, vous devez saisir :-I$(SIMBAENGINE_DIR)/Include/SQLEngine/Executor/ETreeUne fois que vous aurez ajouté le répertoire d'inclusion supplémentaire, vous pourrez compiler le pilote avec n'importe quelle configuration. Le moteur SQL reconnaîtra désormais votre fonction scalaire personnalisée et pourra l'exécuter dans n'importe quelle requête SQL standard.

    Pour optimiser encore davantage vos requêtes (si votre source de données le permet), vous pouvez utiliser la fonctionnalité CQE (Collaborative Query Execution) de Simba afin de transmettre la fonction scalaire personnalisée à votre source de données pour qu’elle l’exécute. Par exemple, avec la requête SELECT * FROM ADDR WHERE ContainsE(LAST_NAME) = 1 et une source de données capable de prendre en charge la fonction scalaire personnalisée dans le clause, nous pouvons mettre en œuvre une Booléen Gestionnaire d'expression pour la transmettre. Si vous deviez implémenter cette transmission, Contient E serait un type de nœud de AE_NT_VX_CUSTOM_SCALAR_FN. (Pour plus d'informations sur CQE et sur la manière de mettre en œuvre des fonctionnalités plus avancées, veuillez consulter notre Guide du développeur du SDK SimbaEngine avec SQLEngine ou « Exécution collaborative des requêtes (CQE) pour les filtres » dans notre base de connaissances.)

    Félicitations (encore une fois) ! Vous disposez désormais d'une fonction scalaire personnalisée dans un pilote ODBC personnalisé (sans trop d'efforts).  Vous voulez en savoir plus ? Téléchargez le SDK SimbaEngine et utilisez le SQLiteDSII exemple de mise en œuvre ScalarFnAdd, ScalarFnConcat et Nom de la fonction scalaire.