DBTimes
Data Warehouse Source
Loader library
Configuration file at a
glance
File Name Pattern – see
also namePattern
Incremental Load Query-
see also dbtable, query, "aggregation.pipeline"
Incremental Load Unique
keys – see also uniqueKeys
Full load vs.
Incremental load vs. Initial load
Example of using Source
Loader
Data Warehouse Source Loader is used to maintain the Data Warehouse Staging area.
Data Warehouse Source Loader Schema version 1
·
Load
configuration is described inside "sourceLoad"
object
·
Load
action is the unit of the load. Load actions are defined as elements of "loadActions" array.
· Configuration file can have additional attributes if needed at the root level in addition to "sourceLoad" object.
This configuration shows a compressed version by
leaving out some options. Refer to Configuration schema for complete definition.
Optional attributes are highlighted:
· "isRemoveDuplicateRows" : false, highlighted in grey and are shown with default values, or
· "loadControl", highlighted in yellow and are not required, or
· “sparkParams”, highlighted in magenta is an example of additional application specific attributes that are not used by the library.
{"sparkParams":
{
"spark.app.name": "NFL Data Source Loader",
"spark.master": "local",
"spark.driver.host": "localhost",
"spark.sql.session.timeZone": "UTC"
},
"sourceLoad":
{"sourceLoadConfigVersion": "1",
"loadActions":
[ {"name" : "Load some file",
"isActive": true,
"fileLoadControl" :
{"parquet" : {},
"dir" : "/dw/data/control/"
},
|
"fileSource" : {"isRemoveDuplicateRows": false, "effectiveDate" : "2020-03-24", |
OR DBMS source |
"dbmsSource" : {"isRemoveDuplicateRows": false, "effectiveDate" : "2020-03-24", |
|||||||||
"fileSystemLocation" : {"dir": "/dw/data/sources/…", "namePattern": "my-file\\.csv" }, |
ORi nternet |
"internetLocation" : { "url": "https://…" "user": "…", "password": "…", "isDisableSslVerification": true }, |
OR custom |
"customLocation" : "methodRetuns":
"FileData", |
"sqlServer" : { "url" : "jdbc:sqlserver://… ", "dbtable" : "…", "numPartitions" : 10, "query": "…”,"prepareQuery": "…”, … }, |
OROracle |
"oracle" :
{ "url" : "jdbc:oracle:thin://… ", "dbtable": "…", "numPartitions": 10, "query": "…”,"prepareQuery": "…", …
}, |
ORMongoDB |
"mongoDb" : "connection.uri" : "jdbc:sqlserver://… ", "database": "…", "collection":
"…", … |
|||
|
"csv" : {
"mode" : "PERMISSIVE", "dateFormat" :
"MM/dd/yyyy", … },"processedFilesDir": "/dw/data/sources/processed/" },
|
"user": "…", "password": "…", "incrementalLoad" : {"uniqueKeys": "( SELECT SomeName, SomeOtherName FROM my-table ) AS tbl" "delimForWatermarkColumns": ":::", }}, |
||||||||||
"fileDestination" :
{"isVersioned" : true,
"parquet" : {},
"path" : "/dw/data/…/file-name",
"previousCopyPath" : "/dw/data/…/prev-copy-file-name"
},
"schema" :
[{ "colName": "SomeName", "colType": "Integer", "isUniqueKey": true, "isPartitionColumn": true }
{ "colName": "SomeOtherName", "colType": "String" },
… ],
"postProcess" :
{
"packageName": "...",
"moduleName": "...",
"methodName": "someMethodForPostProsess",
"stgSources" :
[{ "moniker" : "PlayByPlay",
"description" : "Play by play s/sheet for all games",
"fileSource" : { "path" : "C:\\ASCODE\\DATA\\NFL\\STAGING\\stgPbp.parquet", "parquet" : { } }
}, … ],"applicationSpecific": { }
}, }, {"name" : "Load some other source",
… }, … ] }}
Any field that
starts with two underscores is a comment and is ignored. A value for the
comment field can be of any type.
For
example, this field is a comment
"__previousCopyPath
old" : "/dw/staging/team.prev.parquet"
Since JSON does not allow
for multiline fields and some fields, like SQL SELECT or Mongo DB aggregation,
can be very long, configuration allows for some fields to have value of a
string or array of string.
Example of a long
field with array value
"aggregation.pipeline": [
"[",
" {",
" $project:",
" {",
" _id: 0,",
" numberValue: \"$numberValue\",",
" stringValue: \"$_string_Value_\",",
" booleanValue:
\"$booleanValue\",",
" dateValue: \"$dateValue\",",
" nullValue: \"$nullValue\"",
" }",
" }",
"]"
],
A regular expression of file names to process.
The files can have effective date as part of the name. In that case the files
are processed in a date ascending order.
The effective date must use capturing groups
with specific names for date parts: effectiveDate, year,
month and day. Month and day are optional and each will be
defaulted to 1 if absent.
(?<effectiveDate>(?<year>20[0-9][0-9])(?<month>[0-1][0-9])(?<day>[0-3][0-9]))
The examples of namePattern with effective date (the
second one has a year only, so month and day will be defaulted to 1):
pbp-(?<effectiveDate>(?<year>20[0-9][0-9])-(?<month>[0-1][0-9])-(?<day>[0-3][0-9])).*\.csv
pbp-(?<effectiveDate>(?<year>20[0-9][0-9]))\.csv
The example of namePattern without effective date:
NFL-Teams\\.csv
All the files in the source directory that match
the pattern will be processed and moved to processedFilesDir
If for example the source directory has files
"combine.csv"
"pbp-2013.csv"
"pbp-2013 -
Copy.csv"
"pbp-2014.csv"
"pbp-2014 -
Copy.csv"
"pbp-2015 -
Copy.csv"
"pbp-2016 -
Copy.csv"
The following patttern
"pbp-(?<effectiveDate>(?<year>20[0-9][0-9]))\.csv", will result in processing of
two files
"pbp-2014.csv"
"pbp-2013.csv"
The effective date for the first file will be
2014-01-01, and for the second – 2015-01-01.
When load action defines incremental load, the query
attribute should have a statement that loads data incrementally based on a
watermark column or columns. The watermark column is a column whose previously
loaded MAX value is used to determine the new rows for the incremental load.
Example 1 - SQL Server:
"dbtable"
: "( SELECT *, CONVERT( VARCHAR( 200), CONVERT( BINARY(8), [RowVersion], 1 ), 2 ) AS RowVersionStr
FROM [dbo].[ARMResetTest]
WHERE [RowVersion] > 0x:::RowVersionStr:::
) AS tbl",
where, RowVersionStr is the column in the result set and is used as watermark. The loader will substitute :::RowVersionStr::: (see loadActions.dbmsSource.incrementalLoad.delimForWatermarkColumns ) with the maximum value of previously loaded data from the "fileDestination". For example, if the maximum value for the attribute is 000000000002E784, the above statement will be replaced with "( SELECT *, CONVERT( VARCHAR( 200), CONVERT( BINARY(8), [RowVersion], 1 ), 2 ) AS RowVersionStr FROM [dbo].[ARMResetTest] WHERE [RowVersion] > 0x000000000002E784 ) AS tbl".
Example 2 – MongoDB:"aggregation.pipeline": [
"[",
" {",
" $match: { \"$expr\": { $gt: [ \"$GameDate\", { \"$toDate\": \":::GameDate:::\" } ] } } ",
" },",
" {",
" $project:",
" {",
" _id: 0,",
" \"_id_str\" :{ \"$toString\": \"$_id\" },",
" GameId: \"$GameId\",",
" GameDate: \"$GameDate\",",
" Quarter: \"$Quarter\",",
" Minute: \"$Minute\",",
" Second: \"$Second\",",
" OffenseTeam: \"$OffenseTeam\",",
" DefenseTeam: \"$DefenseTeam\"",
" }",
" }",
"]"
],
where, GameDate is the column in the result set and is used as watermark. The loader will substitute :::GameDate::: (see loadActions.dbmsSource.incrementalLoad.delimForWatermarkColumns ) with the maximum value of previously loaded data found in "fileDestination".
Unique keys is a query
to select unique keys from the database. Unique keys are needed to delete rows
that are no longer in the source.
Example for SQL Server
"uniqueKeys" : "( SELECT SecId, AdjDt FROM [dbo].[ARMResetTest] ) AS tbl"
Example for MongoDB
"uniqueKeys": "[ { $project : { _id: 0, \"_id_str\": { \"$toString\": \"$_id\" } } } ]"
There are two ways to load data from DBMS source
– full and incremental. Full load should not be confused with initial load.
Full load means that the entire source table is read. Full load is always used
for the very first or initial load. For subsequent loads full or incremental
load can be used. For the incremental load the configuration must have the
incremental load section.
|
|
Order of the load in time |
|
|
|
Initial Load |
Subsequent load |
|
Full Load |
a |
a |
|
Incremental Load |
|
a |
Table 1. Applicability
of full/incremental load
"sparkParams" object in this
file is not part of loader configuration schema. Application can have arbitrary
fields outside of "sourceLoad" object.
{
"sparkParams":
{
"spark.app.name": "NFL Data
Source Loader",
"spark.master":
"local",
"spark.driver.host": "localhost",
"spark.sql.session.timeZone": "UTC"
},
"sourceLoad":
{
"sourceLoadConfigVersion": "1",
"loadActions":
[
{
"name" : "Load Team file",
"isActive": true,
"fileLoadControl" :
{
"parquet" :
{},
"dir" : "/dw/data/control/"
},
"fileSource" :
{
"isRemoveDuplicateRows": false,
"fileSystemLocation" :{
"dir": "/dw/data/sources/teaminfo",
"namePattern" : "NFL-Teams\\.csv"
},
"csv" :
{
"header" : true
},
"processedFilesDir": "/dw/data/sources/processed/"
},
"fileDestination" :
{
"isVersioned" : false,
"parquet" :
{},
"path" : "/dw/data/staging/team.parquet",
"previousCopyPath" : "/dw/data/staging/team.prev.parquet"
},
"schema" :
[
{ "colName" : "TeamName","colType" : "String"
},
{ "colName" : "SeasonAbbreviation", "colType" : "String" },
{ "colName" : "SeasonYear",
"colType"
: "Integer" },
{ "colName" : "SeasonTeamName",
"colType"
: "String" },
{ "colName" : "ActiveSince",
"colType"
: "Integer" }
]
},
{
"name" : "Load Players file",
"isActive": true,
"fileLoadControl" :
{
"parquet" :
{},
"dir" : "/dw/data/control/"
},
"fileSource" :
{
"isRemoveDuplicateRows": false,
"fileSystemLocation" :{
"dir": "/dw/data/sources/players",
"namePattern" : "players_2013-12-12.csv"
},
"csv" :
{
"header" : true
},
"processedFilesDir": "/dw/data/sources/processed/"
},
"fileDestination" :
{
"isVersioned" : false,
"parquet" :
{},
"path" : "/dw/data/staging/stgplayers.parquet",
"__ optional-field previousCopyPath"
: "/dw/data/staging/stgpbp.prev.parquet"
},
"schema" :
[
{ "colName" : "name", "colType" : "String", "isUniqueKey" : true },
{ "colName" : "first_name",
"colType"
: "String" },
{ "colName" : "last_name",
"colType"
: "String" },
{ "colName" : "birth_city",
"colType"
: "String" },
{ "colName" : "birth_state",
"colType"
: "String" },
{ "colName" : "birth_country",
"colType"
: "String" },
{ "colName" : "birth_date",
"colType"
: "String", "isUniqueKey" : true },
{ "colName" : "college", "colType" : "String" },
{ "colName" : "draft_team",
"colType"
: "String" },
{ "colName" : "draft_round",
"colType"
: "String" },
{ "colName" : "draft_pick",
"colType"
: "String" },
{ "colName" : "draft_year",
"colType"
: "Integer" },
{ "colName" : "position", "colType" : "String" },
{ "colName" : "height", "colType" : "String" },
{ "colName" : "weight", "colType" : "Integer" },
{ "colName" : "death_date",
"colType"
: "String" },
{ "colName" : "death_city",
"colType"
: "String" },
{ "colName" : "death_state",
"colType"
: "String" },
{ "colName" : "death_country",
"colType"
: "String" },
{ "colName" : "year_start",
"colType"
: "Integer" },
{ "colName" : "year_end", "colType" : "Integer"
}
]
},
{
"name" : "Load ARMReset table",
"isActive": true,
"fileLoadControl" :
{
"parquet" :
{},
"dir" : "/dw/data/control/"
},
"dbmsSource" :
{
"effectiveDate" : "2020-05-13",
"sqlServer" : {
"url" : "jdbc:sqlserver://DESKTOP\\SQLSERVER2019:1433;database=testdb;integratedSecurity=true;",
"dbtable" : "( SELECT
*, CONVERT( VARCHAR( 200), CONVERT( BINARY(8), [RowVersion],
1 ), 2 ) AS RowVersionStr FROM [dbo].[ARMResetTest] WHERE [RowVersion]
> 0x:::RowVersionStr::: ) AS tbl",
"numPartitions" : 10
},
"incrementalLoad" :
{
"uniqueKeys" : "( SELECT SecId, AdjDt FROM [dbo].[ARMResetTest] ) AS tbl",
"delimForWatermarkColumns": ":::"
}
},
"fileDestination" :
{
"isVersioned" : true,
"parquet" :
{},
"path" : "/dw/data/staging/armresettest.parquet",
"previousCopyPath" : "/dw/data/staging/armresettest.prev.parquet"
},
"schema" :
[
{ "colName" : "SecId", "colType": "Integer", "isUniqueKey":
true, "isPartitionColumn" : true },
{ "colName" : "AdjDt", "colType": "Date", "isUniqueKey":
true }
]
}
]
}
}
package com.dbtimes.dw.sourceloader
object DataSourceLoader
validateConfig(appConfig: Config): Unit - validates configuration against schema. Schema version to validate against is defined in "sourceLoadConfigVersion" field in the configuration. All configuration errors must be fixed before proceeding with the load.
Arguments:
appConfig: com.typesafe.config.Config – configuration that has "sourceLoad" as one of the fields. Configuration can include other fields – they will be ignored.
Returns:
UnitThrows an exception if validation failed
Example:
import com.typesafe.config.{ConfigFactory, Config}
val appConfig = ConfigFactory.load(configFileName)
// validate configuration. If
there any configuration error the method will throw an exception
DataSourceLoader.validateConfig(appConfig: Config)
loadData(appConfig: Config): Unit - load data into destination (i.e., staging) files based on the configuration.
Arguments:
appConfig: com.typesafe.config.Config – configuration
Returns:
Seq[String]- collection of strings with errors. If there are no errors the collection will be empty
Example:
// validate
configuration
val appConfig = ConfigFactory.load(configFileName)
val errors: Seq[String] = DataSourceLoader.validateConfig(appConfig: Config)
if ( !errors.isEmpty ) {
val errorMessage = errors.mkString( "Loader
Configuration ERROR(s)\n *", "\n *", "" )
throw
new RuntimeException(errorMessage)
}
For custom file location the configuration
would look like this
"fileSource"
:
{
"customLocation" :
{
"packageName":
"com.dbtimes.nflsourceloader",
"moduleName":
"SrcLoaderNFL",
"methodName":
"sampleForCsvFileCustomLocation",
"methodReturns":
"FileData",
"applicationSpecific": { }
},
"csv" :
{
"header"
: true,
"multiLine":
true,
"quote": "\"",
"escape": "\""
},
"processedFilesDir": "/data/nfl/processed/"
},
The callback function must be implemented in Scala object or Java static method. signature:
callbackForCustomLocation( param: Option[Config] ): String
parameter:
value of "applicationSpecific" object; if "applicationSpecific" object is not defined in configuration - None
returns:
file path or file content depending on configuration "methodReturns" - "FilePath" or "FileData”
For example, for "methodReturns": "FileData"
def sampleForCsvFileCustomLocation( param: Option[Config] ): String = {
"""name,age,city
Alice,30,"New
York"
Bob,25,"Los
Angeles""""
}
or, for "methodReturns": "FilePath",
def sampleForCsvFileCustomLocation( params: Option[Config] ): String = {
s"/some-path/csv-file.csv"
}
In some instances it is needed to do
some additional processing on the source data before that data can be saved to
a destination file.
A separate object in configuration would
define post process, e.g.,
"postProcess" :
{
"packageName": "com.dbtimes.nflsourceloader",
"moduleName": "SrcLoaderNFL",
"methodName": "postProsessPlayers",
"stgSources" :
[{ "moniker" : "PlayByPlay",
"description" : "Play by play s/sheet for all games",
"fileSource" : { "path" : "C:\\ASCODE\\DATA\\NFL\\STAGING\\stgPbp.parquet", "parquet" : { } }
} ],"applicationSpecific": {
"field1": "value1",
"field2": "value2",
}
},
The callback function must be implemented in Scala object or Java static method. signature:
callbackForPostProcess( param1: Option[Config], param2: DataFrame ): DataFrame
parameter 1:
value of "applicationSpecific" object; if "applicationSpecific" object is not defined in configuration - None
parameter 2:
loaded source
returns:
post processed source
If "stgSources" is defined in configuration, each of the sources will be available as a view with the name matching the "moniker" value.
For example, for above "postProcess" object the callback may look like this:
def postProcessPlayers( appSpecificAsOption: Option[Config], dfSource: DataFrame ): DataFrame = {
val spark = SparkSession.builder().getOrCreate() // this gets previously created session
val appSpecific = appSpecificAsOption.get
val field1 = appSpecific.getString("field1") // result - "value1"
val field2 = appSpecific.getString("field2") // result - "value2"
val viewSource = "Players" +
java.util.UUID.randomUUID.toString.replace("-", "_" )
val sql = s"""
|SELECT *
|FROM $viewSource
|WHERE draft_team
IN (SELECT DISTINCT OffenceTeam FROM PlayByPlay ) """.stripMargin
val df = spark.sql(sql)
df
}
Create a new project with the main
like this. The first argument is the configuration file.
import org.apache.spark.sql.SparkSession
import com.typesafe.config.{ConfigFactory, Config}
import com.dbtimes.dw.sourceloader.DataSourceLoader
def main(args: Array[String]) {
val configFileName = args(0)
val appConfig = ConfigFactory.load(configFileName)
// validate configuration
val errors: Seq[String] = DataSourceLoader.validateConfig(appConfig: Config)
if
( !errors.isEmpty ) {
val errorMessage = errors.mkString( "Loader Configuration
ERROR(s)\n *", "\n *", "" )
throw
new RuntimeException(errorMessage)
}
// configure and start Spark …
// run the loader to load data into staging area
val spark = sparkSessionBuilder.getOrCreate()
DataSourceLoader.loadData(srcLoaderConfig)
spark.stop()}