Tuesday, 14 April 2020

T-SQL Tuesday #125 - Unit testing databases - How I Found Value in Unit Testing a Complex Database Procedure


I’m writing this post as a response to Hamish Watson’s T-SQL Tuesday #125 invitation. Hamish’s question is this: is unit testing valuable for implementing in databases?

I'd like to write specifically about two places I’ve considered implementing unit testing: a complex data cleanup procedure, and the mass of stored procedures behind SQL Server Reporting Services reports. I believe that unit testing is absolutely required for a large complex process, and encourages several good practices as a side-effect. In the day-to-day development of the stored procedures behind SSRS reports, I’m less sure of the value of a comprehensive suite of unit tests.

When I started working as a junior SSRS developer nine years ago, I didn’t have any notion that unit testing was a thing. The reports ran just fine, and I fixed them where any errors occurred. There was no need to write a separate piece of code after I’d written a report to validate that it worked. Because the queries were hard-coded into the reports, there was also no way to do any sort of database unit testing, so that wasn’t a concern.

Below I’ve tried to document the phases of my relationship with unit testing my database code, and why I don’t write unit tests for the stored procedures behind SSRS reports.

Phase #1. Discovery, and my response

I’ve worked with application developers who chose not to unit test. When I learned about the importance of unit testing in software development from the outside world - a talk by Hamish Watson, YouTube, blog posts etc, 

I believed it was a given that developers should be writing unit tests. I felt like the developers I worked alongside were not fulfilling their jobs properly. I remember one instance where testing would have helped detect that a feature was nonfunctional earlier, instead of the days or weeks it took to identify. In my newfound understanding of unit testing, they were supposed to be in a position to show me how to write unit tests.

I remember one short conversation between myself and that team about unit testing and situational awareness, where one of the developers looked at me perplexed and maybe slightly disgusted. I don't remember what communication happened in that conversation, but looking back I expect the way I communicated in that conversation was not helpful, or on reflection, fair.

At some point, I realised that I needed to stop beating the drum regarding my conviction that they should be doing unit tests, but I, as a database and reports developer, didn’t need to. I suspect that was in 2017, my Google search history shows that I watched TDD for those who don't need it in that year, a talk I highly recommend, which challenges the mentality I had.

Phase #2. Failure managing a complex data cleansing project.

In 2017 I was tasked with a massive data quality improvement cleanup project. There were many duplicate records in a master table for one of our systems, with many dependencies. Several of them were in tables for areas of that system that I wasn’t familiar with, areas which I barely understood. This level of complexity was far outside of my previous experience.

First I started designing a process that merged duplicate records from the bottom-up using a batch-based approach so that it was fast, fixing all rows in each of the tables in the lowest-levels first then working up, but when I realised that sometimes the parent objects wouldn’t exist under the entry I wanted to merge a duplicate into, I stopped, threw-out the bottom-up progress, and started working top-down. 

Because I wrote much of the procedure from a top-down approach, it took a long time to see results and get any outside feedback. If one of those statements was poorly coded, or a nuance had been overlooked, I could be causing some significant damage, and I might not notice it for days, weeks, or months after the problem had been introduced, likely by reports from the end users who knew the data best.

It was seriously painful for those around me who were waiting on that project to complete - the problem was causing duplicate rows to show on the UI, prevented a process change, and required the developers implement some complex workarounds that I didn’t understand to hide the data quality issue during the months that I was working on this. The flip-flop in architecture and the delays were the result of, in part, knowing how serious this code was, and how unlikely it was that all of my code would just work when I eventually hit that execute button. This anxiety was added to by not being able to hold a picture of the whole system in my head due to its size.

During the development of this system, I became a fan of the television show Can’t Pay We’ll Take it Away, as the task that I’d already sunk so much company time into was beginning to be chronically over-schedule. I also put in some serious overtime to try to get that project complete. I need to note that I felt a significant moral obligation to get this done, rather than scared, and the overtime put in was not requested by my employer.

Phase #3. Regaining control using tSQLt.

I remember working on this project on the afternoon of December 25th, 2018, thinking working on this project was the same thing I had been doing the afternoon of that day a year earlier, and it no longer seemed cool, at all. I needed to find a better way to work. Something more sustainable, that would help me make verifiable progress. I recognised that unit testing could provide that. I need to note that this project wasn’t the only thing I did that year, this work was mostly sidelined by other tasks during business hours.

After watching the Pluralsight course Unit Testing T-SQL Code with tSQLt by Dave Green on the SQL tSQLt testing suite, I implemented tSQLt tests for all of the components of the data cleanup procedures. Some of the tests were written using TDD, and other tests written after-the-fact. This pulled up mistakes, and I fixed those, until the tests all passed. It found a good crop of mistakes early on in procedure development. I love how easy tSQLt is to use to build test suites with, and appreciated being able to see actual vs expected variations in the test outcome.

More significantly, writing unit tests gave me the confidence that I needed when it came to clicking  that green triangle on the execute button. Knowing that I had thoroughly tested the individual components gave me some confidence that it would function as a whole.

tSQLt doesn’t support linked servers, so I did need to move databases and remap synonyms to references in those databases, and there were some loopback issues that came up later in dry runs, but those were the limited final pieces on the end of the testing process.

During that project I also learned the importance of modularization, in my case using inline TVFs, to reduce code complexity, and reduce lines of code. This helped speed up the development of both the unit tests and the actual stored procedures. The practice of breaking complex stored procedures into smaller more testable blocks of code is also a good pattern that the work implementing unit testing reinforced to me.

I still chose to run the merge procedures before daylight, and over several days, but none of the code I had written and tested needed redone, and none of the critical failure I was concerned with eventuated. I was glad to see the project complete without significant data damage, If you ignore the serious schedule overrun, and a minor incident around which fields to keep in one table for a merge conflict.

Phase #4. Post-project, returning to business as usual

I work with over 200 SQL Server Reporting Services reports, and the volume of reports and the wide range of other responsibilities my team is tasked with, has resulted in limited capacity for ownership, instead falling back on taking tickets and responding with a report, without a good understanding of the reports that have already been produced. This has led to some unhelpfully specific reports, some essentially duplicate, and some reports that have not fulfilled their need for a long time, but still get enough user hits for them to show as in use and avoid being pruned away. In other terms, the reports have been regarded as somewhat disposable.

A side-effect of this is that the SQL queries behind these reports are hardcoded into the reports. At 2019’s SQL Saturday in Christchurch, a panel with Hamish Watson and Greg Low advocated unanimously for getting SQL out of hardcoded queries in SSRS reports, and into stored procedures, so they can be validated. We’ve invested some work in pulling the queries out into their own stored procedures, and getting those stored procedures under source control. Now we’re down to a very nice 0 Errors, and a few warnings in our source control project. We now get a warning if we reference an object that doesn’t exist. It is an easy way to make a change to where an external synonym points, and get quick notice if that object isn’t recognised. This helps me have some confidence the reports we do have under source control will run when requested.

Writing unit tests for all of the 200+ stored procedures behind these reports, as we interact with them, seems like unnecessary overhead. It would tell us if the output changes unexpectedly, but enough test data to handle the different edge cases and paths in the reports seems like a lot of resource that we don’t have right now. The SSDT database project warns about invalid object references, which I have found to be a great value for detecting broken reports.

We don’t often have report failures causing us to scramble, needing rework due to poorly code quality or invalid references. Users are somewhat happy to tell us when reports aren’t behaving in the way they expected, and these deviations are usually due to a difference in understanding between the user and the person who wrote the report.

I do believe knowing if code is in error is a key element of situational awareness, alongside good monitoring infrastructure. We have our reporting stored procedures in an SSDT project with no errors; this doesn’t test that the procedures are functional and operating correctly, as unit tests would, but it gets most of the way there with little effort needed to maintain. Adding unit tests would tell us if a stored procedure has deviated from known good behavior, given a known input and output, so I do see some value there.

Why I do not have unit tests for the stored procedures behind my SSRS reports

We are not facing an issue regarding the reliability of the existing report code. The issue we do have is that we don’t have coherency over our enterprise reporting solutions. Users don’t know where to find reports, or which ones to trust for their particular need. That is where we need to be investing resources right now - learning what needs each report fulfills, and learning how we can better facilitate our users’ need to find the information they’re looking for, over our various solutions.

Back when I started, when I was starting out as a database and report developer, I had nothing else on my plate. I could focus just on writing reports, maybe I could have implemented unit testing then. Now, I’m learning how to be a somewhat acceptable manager to my direct report, improving workflow, optimising use of the BI/reporting solutions, producing BI work, and managing other database-related priorities. I am also now responsible for influencing the patterns of those who work around me, so maybe I should be doing it to encourage them.


Summary

I’ve seen the wonders of unit testing with a big-bang complex project, but in the slow burn of day-to-day report development, I feel that the competing priorities need my focus more. I’d like to do unit testing of my reporting stored procedures, but I don’t think implementing this is my highest need right now.

Sunday, 29 March 2020

Getting C# Jupyter Notebooks running on Ubuntu 18.04

Why C# on Jupyter Notebooks?

I’ve heard of Jupyter, or Jupyter-like notebooks often. I've briefly evaluated the SQL Server notebooks in Azure Data Studio, and often hear of them used in data science.

The primary notebook platform, Jupyter notebook, only supports Python, and I haven’t yet invested in learning that language. This has previously been a stumbling block for me learning further about notebooks.

At a recent SQL Saturday precon, notebooks were heavily used as a part of the training material, and I got to experience enough to understand the benefit of using notebooks: annotated, byte-size, interactive, hands-on documentation.

I've been planning an introduction presentation to Azure Cosmos DB for our web developers, and Jupyter notebooks seem to be a great fit for that demo, with the exception that Jupyter notebooks use Python, not C#, the developers' native language. Why not use Visual Studio / VS Code? I don't want the web developers to be judging my console application development abilities, I want them to see how the Cosmos DB client can be interacted with in C#.

Azure Cosmos DB has a C# Notebook sample, but that appears to be broken at present, and I can’t work that out - but the idea is simple enough - it is just a notebook running C#. After a little searching on Google, I found that the .NET team have put out instructions on getting a C# kernel (runner) installed in standard Jupyter Notebooks. Here’s the steps I followed to get that up and running.

Steps to creating a C# Jupyter Notebooks environment running on Ubuntu:

Download and Install Anaconda

  • Download the Anaconda installer from https://www.anaconda.com/distribution/#linux (I chose the Python 3.7 installer) 
  • Make the script executable: Right-click on the .sh installer file, and select Properties > Permissions > Allow executing file as program. 
  • Open Terminal, navigate to the folder containing the Anaconda installer, and execute the .sh file 
  • I ran the two commands given as optional in the Anaconda installer. 
  • Run jupyter notebook from Terminal. All well, Jupyter Notebook will load. 
  • Select New, and note that only “Python 3” is an option. Additional engines, called “kernels” can be installed to add additional languages to this list. 
  • In Terminal, press CTRL+C to kill Jupyter Notebook.

Install dotnet interactive and enable C# and PowerShell support to Jupyter Notebooks

  • Install the .NET 3.1 SDK by following the Register Microsoft Key and Feed and Install the .NET Core SDK steps at Install .NET Core on Ubuntu 18.04 package manager - .NET Core 
  • Run the command to install dotnet interactive at dotnet/interactive 
  • Run the command to add the C# and Powershell kernels to Jupyter
  • Notebooks ( dotnet interactive jupyter install ) 
  • Check the C# and Powershell kernels have been added with the command jupyter kernelspec list 
  • Run the command jupyter notebook from Terminal again 
  • Select the New menu item, and you should now see “.NET (C#)” as an option, select this. 
  • Enter Console.WriteLine("Hello from C#!"); into the box beside In [ ]:, and click the Run button. 
  • The In [ ]: text should change to In [ * ]: text to indicate that block is now running.
You now have a C# Jupyter Notebook running in Ubuntu. Switch to Powershell, replace that sample with Write-Host("Hello from PowerShell!");

There we have it - below is a screenshot of the C# tutorial Tutorial: Build a .NET console app to manage data in Azure Cosmos DB SQL API account running successfully from a Jupyter notebook.

Wednesday, 12 July 2017

Solution to SQL Agent Job "Error retrieving WMI variable WMI(TextData)"

I've been trying to implement SQL Agent Alerts on disk growth for a day now.

I've followed a few different guides on WMI alerting, but every time the alert fires, the SQL Job fails with the below error, and trying to play with different methods to ESCAPE the WMI tokens was unfruitful.

Unable to start execution of step 1 (reason: Error retrieving WMI variable WMI(variable name here): 0x80041002).  The step failed.

SQL Junkie Share's blog had the answer. I needed to go to my SQL Agent properties, and under Alert System tick "Replace tokens for all job responses to alerts". Thanks Akhil!

Monday, 20 February 2017

Adding an identity column or column with a default constraint to a table that already has data

Today I had a coworker ask me to review code that had a select statement moving data out of a table, truncating the table, adding an new column with identity and a default constraint, then re-inserting the data into the table. His explanation was that he couldn't add an identity column or a column with a default constraint to a table that contained data.

I displayed to him that moving data out of the table isn't needed to add either a default constraint or identity.

Here's an example of how to do that:

Create a table containing existing data:

create table Customer (
Firstname nvarchar(200)
, Surname nvarchar(200)
)

insert into customer(
firstname
, surname
)
select 'Bill', 'Palmer'
union all select 'Sarah', 'Moulton'
union all select 'Wes', 'Davis'

select firstname
, surname
from customer

There's no problem with adding an identity column to an existing table, except perhaps the time it takes to run this statement - keep that in mind if you're dealing with a very large table.

alter table customer add id int identity(1,1)

select id
, firstname
, surname
from customer

That query will succeed, and return the results of a select statement that includes the new ID column, with values!

Adding a column with a default constraint to an existing table requires a little trick. For this example, let's assume we want to add a bit column to customer, indicating whether the customer is a VIP. 

If you do the usual command to add a new column with a default constraint, as below, you may be surprised.

alter table customer add isVIP bit default(0)

select id
, firstname , surname , isVIP
from customer


Adding the default constraint will succeed, but the returned result-set will have null values in the "isVIP" column for the existing rows. To populate existing rows with the default value, run this statement instead:

alter table customer add isVP bit default(0) not null

select id
, firstname
, surname
, isVP
from customer

The existing rows with be populated your default value, and return that value in your result-set. 

If you want this column to allow NULL values, run the below statement to set the column to nullable, keeping the recently created default constraint.

alter table customer alter column isVP bit null

Monday, 13 February 2017

Identifying unencrypted SQL connections

I'm working on implementing encrypted connections within my team to protect against packet sniffing, however a few developers seem behind on implementing encrypted connections. The below query helped me identify those developers for follow-up, and gave me the information they needed to identify which application was making the unencrypted connections.

select sessions.login_name
, sessions.program_name
, sessions.host_name
, sessions.login_time
, sessions.last_request_start_time
, sessions.status
, sessions.cpu_time
from sys.dm_exec_connections as connections
  inner join sys.dm_exec_sessions as sessions
  on connections.session_id = sessions.session_id
where connections.encrypt_option = 'FALSE'
order by sessions.login_name


Resolving "Cannot create an instance of OLE DB provider "OraOLE.DB.Oracle" after changing the SQL Server Agent account.

After an SQL Server Agent service account change, scheduled jobs that call an Oracle linked server began failing with the error

Executed as user: <username>. Cannot create an instance of OLE DB provider "OraOLEDB.Oracle" for linked server <linked server name>. [SQLSTATE 42000] (Error 7302).  The step failed.

The stored procedures called by those jobs executed as expected when I executed them from my account.

No ACCESS DENIED issues came up in Process Monitor while attempting to make the connection.

After some websurfing, this MSDN blog suggested modifying the MSDAINITIALIZE security settings to give the agent account all permissions on both Launch and Activation Permissions, Access Permissions and Configuration Permissions.

After doing that, the error changed to

Executed as user: <username>. The OLE DB provider "OraOLEDB.Oracle" for linked server <linked server name> reported an error. Access denied. [SQLSTATE 42000] (Error 7399)  Cannot get the column information from OLE DB provider "OraOLEDB.Oracle" for linked server <linked server name>. [SQLSTATE 42000] (Error 7350).  The step failed.

Progress! Searching for that error message took me to this SQL Server Central post, where a user suggested ticking "Allow inprocesses" for the Oracle linked server provider.


Success! The job now completed as expected.


Tuesday, 17 February 2015

ReportServer.dbo.ExecutionLogStorage table only shows a couple of months back

In my workplace we use the reports based off the SSRS Reporting Services database ExecutionLogStorage to identify report usage and alert us to report execution failures, in the form of the below statement to identify how often different reports have been used, how long they take to run, and how often they fail:

select Catalog.path, ExecutionLogStorage.*
from ExecutionLogStorage
inner join Catalog
on ExecutionLogStorage.ReportID = Catalog.ItemID

Unfortunately, on a server that went online some-time last year, the report execution logs are very limited. After doing some research I found the answer here (Microsoft TechNet article).

If the query below returns anything other than -1, your report execution logs will be being deleted after the resulting number of days.

select *
from ConfigurationInfo
where name = 'ExecutionLogDaysKept'

If it is, update it to -1 to stop historical logs from being deleted.

As to how to restore the deleted ExeuctionLogStorage entries, you'll need to restore a copy of the database to a temporary location, and run insert operations to re-add those rows to the table.