Microsoft SQL Server R Services - sp_execute_external_script - I


This post is part of a series of blog-posts about Microsoft SQL Server R Services:

  1. Microsoft SQL Server 2016 R Services Installation
  2. Microsoft SQL Server R Services - Internals I
  3. Microsoft SQL Server R Services - Internals II
  4. Microsoft SQL Server R Services - Internals III
  5. Microsoft SQL Server R Services - Internals IV
  6. Microsoft SQL Server R Services - Internals V
  7. Microsoft SQL Server R Services - Internals VI
  8. Microsoft SQL Server R Services - Internals VII
  9. Microsoft SQL Server R Services - Internals VIII
  10. Microsoft SQL Server R Services - Internals IX
  11. Microsoft SQL Server R Services - Internals X
  12. Microsoft SQL Server R Services - Internals XI
  13. Microsoft SQL Server R Services - Internals XII
  14. Microsoft SQL Server R Services - Internals XIII
  15. Microsoft SQL Server R Services - Internals XIV
  16. Microsoft SQL Server R Services - Internals XV
  17. Microsoft SQL Server R Services - Internals XVI
  18. Microsoft SQL Server R Services - Internals XVII
  19. Microsoft SQL Server R Services - Internals XVIII
  20. Microsoft SQL Server R Services - Internals XIX
  21. Microsoft SQL Server R Services - Internals XX
  22. Microsoft SQL Server R Services: sp_execute_external_script - I (this post)
  23. Microsoft SQL Server R Services - sp_execute_external_script - II
  24. Microsoft SQL Server R Services: sp_execute_external_script - III last post in the series

This post is the 22:nd, post about Microsoft SQL Server R Services. This journey started about a year ago when I thought it would be a good idea to write a couple of blog posts about, what then went under the name of, Microsoft SQL Server 2016 R Services. Never in my wildest fantasies did I think I would write over 20 posts.

NOTE: Since the introduction of Microsoft SQL Server R Services, it has been renamed to Microsoft SQL Server Machine Learning Services.

This series began with a discussion about how to install Microsoft SQL Server R Services and continued with the internals of how it works internally in SQL Server and the launchpad service. Throughout the series, we have used sp_execute_external_script when investigating what goes on. So, I thought that having a couple of posts looking at sp_execute_external_script would be a good way to finish this series off, and this post is the first of two about sp_execute_external_script.

Housekeeping

Before we "dive" into today's topics let us look at the code and the tools we use today. This section is here for those of who want to follow along in what we are doing in the post.

Helper Tools

To help us figure out the things we want, we use Process Monitor, and WireShark:

Code

The code below sets up the database and creates some tables with some data. The data is based on the Iris dataset:

Database Objects
USE master;
GO
DROP DATABASE IF EXISTS IrisTestDb;
CREATE DATABASE IrisTestDb;
USE IrisTestDb;
GO
DROP TABLE IF EXISTS dbo.tb_irisdata_full;
CREATE TABLE dbo.tb_irisdata_full
(
RowID int identity primary key, SepalLength float not null,
SepalWidth float not null, PetalLength float not null,
PetalWidth float not null, Species varchar(100) null
);
GO
DROP TABLE IF EXISTS dbo.tb_irisdata_even;
CREATE TABLE dbo.tb_irisdata_even
(
RowID int identity primary key, SepalLength float not null,
SepalWidth float not null, PetalLength float not null,
PetalWidth float not null, Species varchar(100) null
);
GO
DROP TABLE IF EXISTS dbo.tb_irisdata_uneven;
CREATE TABLE dbo.tb_irisdata_uneven
(
RowID int identity primary key, SepalLength float not null,
SepalWidth float not null, PetalLength float not null,
PetalWidth float not null, Species varchar(100) null
);
GO
INSERT INTO dbo.tb_irisdata_full( SepalLength, SepalWidth,
PetalLength, PetalWidth, Species)
EXECUTE sp_execute_external_script
@language = N'R'
, @script = N'iris_data <- iris;'
, @input_data_1 = N''
, @output_data_1_name = N'iris_data';
--split up the Iris data
INSERT INTO dbo.tb_irisdata_even( SepalLength, SepalWidth,
PetalLength, PetalWidth, Species)
SELECT SepalLength, SepalWidth, PetalLength, PetalWidth, Species
FROM dbo.tb_irisdata_full
WHERE RowID % 2 = 0
INSERT INTO dbo.tb_irisdata_uneven( SepalLength, SepalWidth,
PetalLength, PetalWidth, Species)
SELECT SepalLength, SepalWidth, PetalLength, PetalWidth, Species
FROM dbo.tb_irisdata_full
WHERE RowID % 2 = 1
/
SELECT * FROM dbo.tb_irisdata_full
SELECT * FROM dbo.tb_irisdata_even
SELECT * FROM dbo.tb_irisdata_uneven
/

Code Snippet 1: Creating Database Objects and Generating Data

The code in Code Snippet 1 should be fairly self explanatory. The reason why we split up the full Iris dataset is to later on in this post handle multiple datasets.

sp_execute_external_script

The first thing to do is to remind ourselves why we need sp_execute_external_script at all? Well, as opposed to SQLCLR, the external script engine is not embedded in SQL Server, but sits outside of SQL Server and runs in its own process - it is, (wait for it...), external :). So, if the external engine runs outside of SQL Server there needs to be a bridge between SQL and the engine - and sp_execute_external_script is that bridge.

In most of the blog posts in this series we have used sp_execute_external_script something like so (or with variants thereof):

Execution of R Script
EXEC sp_execute_external_script @language =N'R',
@script=N'OutputDataSet<-InputDataSet',
@input_data_1 =N'SELECT 42'
WITH RESULT SETS (([TheAnswer] int not null));
GO

Code Snippet 2: Use of sp_execute_external_script

When we compare Code Snippet 2 with what the full signature of sp_execute_external_script, as in Code Snippet 3, we see that we have used sp_execute_external_script in a somewhat simplistic way:

sp_execute_external_script
sp_execute_external_script
@language = N'language',
@script = N'script',
[ @input_data_1 = ] 'input_data_1'
[ , @input_data_1_name = ] N'input_data_1_name' ]
[ , @output_data_1_name = N'output_data_1_name' ]
[ , @parallel = 0 | 1 ]
[ , @params = ] N'@parameter_name data_type [ OUT | OUTPUT ] [ ,...n ]'
[ , @parameter1 = ] 'value1' [ OUT | OUTPUT ] [ ,...n ]
[ WITH <execute_option> ]
[;]

Code Snippet 3: Full Signature of sp_execute_external_script

In a majority of the blog posts, the only parameters used are: @language, @script and sometimes @input_data_1, but as we see in Code Snippet 3, there are more:

So, what are these parameters intended use? Let us find out.

@language

By now we should know what the @language parameter is for. It is to tell the launchpad service which launcher dll to initialise, which in turn targets the correct external engine. At the time of writing, there are two external engines which we can use; R and Python, and I would expect Microsoft to add other engines over time. From my side, I would love to see being able to call into .NET - outside of the SQL Engine.

Figure 1: External Script and Language

Figure 1 illustrates how the launchpad service chooses which launcher dll to launch based on the @language parameter.

NOTE: Microsoft SQL Server R Services - Internals II covers more about the launchpad service and different types of launchers.

The @language parameter is a required parameter.

@script

The @script parameter defines the code the external engine executes, and is a required parameter:

Script
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- iris
setosa <- iris[iris$Species == "setosa",]
meanSepWidth <- mean(setosa$Sepal.Width)
cat(paste("Seposa sepal mean width: ", meanSepWidth))';

Code Snippet 4: Example of @script Parameter

In Code Snippet 4 we use the Iris dataset, and we calculate the mean of the sepal width for the setosa species. We pass the script as a literal in the @script parameter. Passing the script as a literal is quite common, and you can see it in most examples of sp_execute_external_script. It is however not required to pass the script as a literal; it can be passed as a variable as well as loaded from a source file. Code snippets 5 and 7 show examples of this:

Script Variable
DECLARE @scriptParam nvarchar(max) =
N'iris_dataset <- iris
setosa <- iris[iris$Species == "setosa",]
meanSepWidth <- mean(setosa$Sepal.Width)
cat(paste("Seposa sepal mean width: ", meanSepWidth))';
EXEC sp_execute_external_script
@language = N'R',
@script = @scriptParam;

Code Snippet 5: Script Passed in via a Variable

So, in Code Snippet 5 we see how the script is passed in as a variable (@scriptParam). A use case for this can be that you have scripts stored in a table in a database, and when you want to execute the script you load the particular script into a variable.

Onto loading a script via a source file; let us assume we have an R source file named iris_r.r with code like so:

R
iris_dataset <- iris
setosa <- iris[iris$Species == "setosa",]
meanSepWidth <- mean(setosa$Sepal.Width)
cat(paste("Seposa sepal mean width: ", meanSepWidth))

Code Snippet 6: R Source File Content

The file is at: c:\rscripts. The way to execute sp_execute_external_script based on this looks like so:

Script Source File
EXEC sp_execute_external_script
@language = N'R',
@script = N'source("c:/rscripts/iris_r.r")';

Code Snippet 7: Script Passed in Through Source File

We see in Code Snippet 7 how we use the R command source to load the source code. The source command causes R to accept its input from the named file. Input is read and parsed from that file until the end of the file is reached, then the parsed expressions are evaluated sequentially in the chosen environment. When you use the source command, like in Code Snippet 7, the file path needs to be accessible by the external engine.

You may note how, in Code Snippet 7, the file path is defined like you would do in Linux: c:/rscripts/iris_r.r. To do it this way is not required, you can use Windows path definitions, but you have to escape the \, like this: c:\\rscripts\\iris_r.r.

Oh, something to keep in mind is that the values for @language, @script, @input_data_1, @input_data_1_name, @output_data_1_name and @params all have to be explicit nvarchar. What I mean with that is when you assign a value to the particular parameter, it has to be DECLARE:ed as an nvarchar. An example of this is Code Snippet 5 and the @scriptParam parameter. If the parameter value is not DECLARE:ed as an nvarchar, it has to prepended with N as we see for example in Code Snippet 7: @language = N'R' and @script = N'source("c:/rscripts/iris_r.r"). If you do not DECLARE or prepend with N you get an error like this:

Figure 2: Error non nvarchar Parameter Value

One final thing about the @script parameter. Remember how we, back in Internals - X, discussed how SQL Server sends the actual script to the SqlSatellite via a socket connection? When we supply the script through a file name, the data sent is: source("c:/rscripts/iris_r.r"). It may be obvious, but I thought I would mention it regardless.

NOTE: Later on in this post we talk some more about what SQL Server sends to the SqlSatellite.

@input_data_1

The @input_data_1 parameter, which is optional, specifies the input data used by the external script. You can only use a T-SQL SELECT statement to generate the data; so no stored procedure calls, but SELECT against views or table-valued functions work fine:

Input Data
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- InputDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_full'

Code Snippet 8: Using @input_data_1 with straight SELECT

In Code Snippet 8 wee set how the @input_data_1 parameter contains a SELECT statement against the dbo.tb_irisdata_full table, and the statement executes during the sp_execute_external_script execution (in Internals - XIII we discussed when the statement was executed). The dataset generated by the query is referred to in the script as InputDataSet, and we discuss the naming later in this post. The SELECT does not have to be against a table, it can be, as I mentioned above, against a view or a user-defined function as well.

So this is straight-forward so far, but what about if you need multiple datasets loaded into the external engine? My first thought when I saw sp_execute_external_script and the @input_data_1 parameter, was that if there is an @input_data_1 parameter, then there surely must be @input_data_2, @input_data_n parameters as well. Well, there are not, bummer, so what do we do?

A thought would be to try and execute multiple statements from the same parameter and then in the script, parse out the different datasets, something like this: `@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_even; SELECT * FROM dbo.tb_irisdata_uneven'. Good suggestion but no cigar, because when you try - this happens:

Figure 3: Multi Statement Error

The error we see in Figure 3 is not very descriptive, but what it means is that you cannot have multiple SELECT statements generating multiple datasets. What you can do however is to use UNION or UNION ALL:

UNION ALL
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- InputDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_even UNION ALL
SELECT * FROM dbo.tb_irisdata_uneven';

Code Snippet 9: SELECT with UNION ALL

The code in Code Snippet 9 uses the UNION ALL to combine the results of two or more queries into a single dataset. However, as useful as that is, sometimes that does not work: what if the two queries do not contain the same data types, or if you need two (or more) individual data-sets? In that case, the solution is to retrieve data from inside the script (in addition to the input data from @input_data_1).

To retrieve data from within the external script, we can use ODBC (in R it is the RODBC package) or the highly optimized Microsoft RevoScaleR package. In the following example I use RevoScaleR:

RevoScaleR
EXEC sp_execute_external_script
@language = N'R',
@script = N'
# set up connection string
sqlConnString <- "Driver=SQL Server;server=win10-dev;
database=IrisTestDb;uid=<some_uid>;pwd=<some_pwd>"
# define the data
mydata <- RxSqlServerData(table = NULL,
sqlQuery = "SELECT *
FROM dbo.tb_irisdata_uneven",
connectionString = sqlConnString)
# open the dataset
rxOpen(mydata)
# read the data
iris_uneven <- rxReadNext(mydata)
versicolor <- iris_uneven[iris_uneven$Species == "versicolor",]
meanSepWidthVersi <- mean(versicolor$SepalWidth)
# get the data from the input data set
iris_dataset <- InputDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
# output the data
cat(paste("Seposa sepal mean width:", meanSepWidth, ".",
"Versicolor sepal mean width:", meanSepWidthVersi))',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_even';

Code Snippet 10: Multiple Data Sets using RevoScaleR

The code in Code Snippet 10 shows how we read in data within the script by the use of RevoScaleR functionality together with the data from the @input_data_1 parameter, and how we subsequently calculate mean on the two data-sets.

Cool, we have now seen how we pass data into the external script via the @input_data_1 parameter, and how multiple data-sets can be used by querying data from inside the script. What about returning data to the caller, you know - like we quite often do from stored procedures?

@input_data_1_name & @output_data_1_name

You may now wonder what this section has to do with returning data to the caller? Don't worry; we get there soon!

So, there are some parameters in sp_execute_external_script that we have not used yet, and @input_data_1_name and @output_data_1_name, which both are optional, are two of them.

@input_data_1_name

We use this parameter to explicitly name the variable in the script that contains the data we pass into the external engine. But, hang on, we have not used the @input_data_1_name parameter in the various examples in this post where we pass data to the engine, and it still works? Ah, look in code snippets 8, 9 and 10, and you see a line of code: iris_dataset <- InputDataSet. Here InputDataSet defines the input data, and it so happens that the default value of @input_data_1_name is InputDataSet.

So, let us see what happens if we change some things. Copy the code in Code Snippet 8 and change it to this:

Input Name
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- InputDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_full',
@input_data_1_name = N'MyDataSet'

Code Snippet 11: Invalid Input Name

As you see in Code Snippet 11 we assign a name to the @input_data_1_name parameter, but in the script we still use InputDataSet. When we execute the following error happens:

Figure 4: Non Existent Input Data Object

So the error is that the script cannot find the InputDataSet object. Change the line: iris_dataset <- InputDataSet to: iris_dataset <- MyDataSet, and execute again. Now all is OK.

How does the script know what the input data is, e.g. how does it know in Code Snippet 11 that the data belongs to the MyDataSet variable? Well this comes back to, at least indirectly, what we discussed in Internals - X and how SQL Server sent the script, plus other information, to the SqlSatellite.

Just for fun, let us revisit some of what we did in Internals - X:

Figure 4: Process Monitor Output

The outlined packet in Figure 4 is the packet containing the script SQL Server sent to the SqlSatallite. Notice the size of the packet and also make a note of the Path column and the last value (in the figure it is 50887). That value is the port with which SQL Server communicates with the SqlSatellite. We use this now in WireShark to try and see if we can get any more information about what is happening.

So, let us switch over to WireShark:

Execute the code in Code Snippet 8 again, and look at the output from WireShark. In the output is a packet with the same length as the highlighted packet in Figure 4. This packet is the packet SQL Server sends to the SqlSatellite with the script (as discussed above). When we look at the data part of that packet as a hex dump, the 64 first bytes look like this:

Hex Dump I
01 00 05 80 48 03 00 00 5c 46 19 98 6a 3f 9a 4f ....H... \F..j?.O
9d b6 25 1f 62 7f 8b 50 00 00 06 00 00 00 00 00 ..%.b..P ........
00 00 00 00 1a 00 49 00 6e 00 70 00 75 00 74 00 ......I. n.p.u.t.
44 00 61 00 74 00 61 00 53 00 65 00 74 00 00 00 D.a.t.a. S.e.t...

Code Snippet 12: WireShark Output I

In Code Snippet 8 we did not define a name for the input data set, but in the script, we used InputDataSet as a variable name - as that is the default. When looking at the hex dump in Code Snippet 12 we see the name InputDataSet 38 bytes into the packet. So it looks like the name we give the data set is being part of the script data SQL Server sends to the SqlSatellite. We can potentially prove that theory by executing the code in Code Snippet 11 (remember to change the code that refers to the data set to: iris_dataset <- MyDataSet) and see what WireShark outputs:

Hex Dump II
01 00 05 80 3c 03 00 00 82 a4 40 22 b4 3a 05 4b ....<... ..@".:.K
9a d0 03 f2 df 19 96 4f 00 00 06 00 00 00 00 00 .......O ........
00 00 00 00 14 00 4d 00 79 00 44 00 61 00 74 00 ......M. y.D.a.t.
61 00 53 00 65 00 74 00 00 00 1c 00 4f 00 75 00 a.S.e.t. ....

Code Snippet 13: WireShark Output II

Ok, so when we look at the code in Code Snippet 13 we see how the input data set name that we assigned through the @input_data_1_name parameter is part of the packet.

So we can answer the question how the script knows what variable contains input data by pointing to how SQL Server passes the name of the input data variable as part of the script. Worth noticing is how, in the two packets above, at 36 bytes into the packet, the size of the variable name (in double bytes) plus two bytes are defined. In Code Snippet 12 it is hex: 1a (decimal 26) , and in Code Snippet 13 it is hex: 14 (decimal 20).

@output_data_1_name

Finally, we get to how the script knows to return resultsets to the caller. Let us assume that you want to do some calculations on the data passed in through the MyDataSet variable in Code Snippet 11 and then return that data. Maybe something like so:

Return Data
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- MyDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))
multiplier <- 5
iris_dataset$SepalLength <- iris_dataset$SepalLength * multiplier
OutputDataSet <- data.frame(iris_dataset$SepalLength)',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_full',
@input_data_1_name = N'MyDataSet'

Code Snippet 14: Return a Result Set

When you execute the code in Code Snippet 14, part of the resultset looks like this:

Figure 5: Result Set

In Figure 5 we see how a resultset comes back, and looking at the code in Code Snippet 14, and knowing about InputDataSet, we assume that the reason a resultset comes back is that of the OutputDataSet variable in the script. It turns out that assumption is correct.

Similar to the input data variable and its default name, the output data variable has a default name: OutputDataSet. You can change this name by setting the @output_data_1_name variable to a name of your choice, and reference that name in the script:

Output Data Name
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- MyDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))
multiplier <- 5
iris_dataset$SepalLength <- iris_dataset$SepalLength * multiplier
SepalLengthMult <- data.frame(iris_dataset$SepalLength)',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_full',
@input_data_1_name = N'MyDataSet',
@output_data_1_name = N'SepalLengthMult';

Code Snippet 15: Customised Result Set Name

In Code Snippet 15 we changed the name of the output data variable to SepalLengthMult and referenced it in the script.

The script engine (and script) knows about the variable name in the same way as it does for the input data variable - it is part of the script packet that SQL Server sends to the SqlSatellite. Below is a partial hex dump of the packet SQL Server sends when we execute the code in Code Snippet 15:

Hex Dump III
01 00 05 80 66 04 00 00 e5 fe 5a d3 1f e3 7d 4a ....f... ..Z...}J
8e 68 95 ee 31 59 75 38 00 00 06 00 00 00 00 00 .h..1Yu8 ........
00 00 00 00 14 00 4d 00 79 00 44 00 61 00 74 00 ......M. y.D.a.t.
61 00 53 00 65 00 74 00 00 00 20 00 53 00 65 00 a.S.e.t. .. .S.e.
70 00 61 00 6c 00 4c 00 65 00 6e 00 67 00 74 00 p.a.l.L. e.n.g.t.
68 00 4d 00 75 00 6c 00 74 00 00 00 38 01 00 00 h.M.u.l. t...8...

Code Snippet 16: WireShark Output OutputData

In the hex dump in Code Snippet 16 we see the name of the output dataset variable starting at byte 60. We can also see at byte 58 how the length of the output data variable is defined, plus two bytes: hex 20.

So, @output_data_1_name specifies the name of the variable in the external script that contains the data to be returned to SQL Server when the stored procedure completes. There is an implicit expectation of the data type of the output; for R the expectation is a data frame, and for Python, it is a pandas data frame.

Oh, a last thing about the output data. When you look at Figure 5 you see that the resultset has no column names. By default sp_execute_external_script returns resultsets with unnamed columns as column names within a script are local. If you want to name columns you use the WITH RESULTS SET clause of the EXECUTE:

Named Column
EXEC sp_execute_external_script
@language = N'R',
@script = N'
iris_dataset <- MyDataSet
setosa <- iris_dataset[iris_dataset$Species == "setosa",]
meanSepWidth <- mean(setosa$SepalWidth)
cat(paste("Seposa sepal mean width: ", meanSepWidth))
multiplier <- 5
iris_dataset$SepalLength <- iris_dataset$SepalLength * multiplier
SepalLengthMult <- data.frame(iris_dataset$SepalLength)',
@input_data_1 = N'SELECT * FROM dbo.tb_irisdata_full',
@input_data_1_name = N'MyDataSet',
@output_data_1_name = N'SepalLengthMult'
WITH RESULT SETS ((LengthMultiplied float));

Code Snippet 17: Define Column Name

In Code Snippet 17 we defined the column coming back with a name of LengthMultiplied and that the column should have a data type of float. When we execute the code we see this:

Figure 6: Result Set

Looking at Figure 6 we see how we received a result set consisting of one column named LengthMultiplied. WITH RESULT SETS is not something specific for sp_execute_external_script but an option on the EXECUTE statement. If you are interested in what happens when you execute sp_execute_external_script with the WITH RESULT SETS option, I covered it in Internals - XVIII.

Summary

In this post we have looked at some of the parameters sp_execute_external_script expects:

All of the above parameter values have to be of the NVARCHAR data type.

In next blog post we cover some of the remaining parameters for sp_execute_external_script.

~ Finally

If you have comments, questions etc., please comment on this post or ping me.

Share this Post:

Twitter | Google+ | LinkedIn

Blog Feed:

To automatically receive more posts like this, please subscribe to my RSS/Atom feed in your feed reader!