Other Tests

SQLConnection and SQLStatement

A SQLConnection object encapsulates a raw database connection together with the database catalog.

using FunSQL: SQLConnection, SQLCatalog, SQLTable
using Pkg.Artifacts, LazyArtifacts
using SQLite

const DATABASE = joinpath(artifact"synpuf-10p", "synpuf-10p.sqlite")

raw_conn = DBInterface.connect(SQLite.DB, DATABASE)

person = SQLTable(:person, columns = [:person_id, :year_of_birth])

catalog = SQLCatalog(person, dialect = :sqlite)

conn = SQLConnection(raw_conn, catalog = catalog)
#-> SQLConnection(SQLite.DB( … ), catalog = SQLCatalog(…1 table…, dialect = SQLDialect(:sqlite)))

SQLConnection delegates DBInterface calls to the raw connection object.

DBInterface.prepare(conn, "SELECT * FROM person")
#-> SQLite.Stmt( … )

DBInterface.execute(conn, "SELECT * FROM person")
#-> SQLite.Query{false}( … )

When DBInterface.prepare is applied to a query node, it returns a FunSQL-specific SQLStatement object.

using FunSQL: From

q = From(:person)

stmt = DBInterface.prepare(conn, q)
#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ))

DBInterface.getconnection(stmt)
#-> SQLConnection( … )

DBInterface.execute(stmt)
#-> SQLite.Query{false}( … )

DBInterface.close!(stmt)

For a query with parameters, this allows us to specify the parameter values by name.

using FunSQL: Get, Var, Where

q = From(:person) |>
    Where(Get.year_of_birth .>= Var.YEAR)

stmt = DBInterface.prepare(conn, q)
#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ), vars = [:YEAR])

DBInterface.execute(stmt, YEAR = 1950)
#-> SQLite.Query{false}( … )

DBInterface.close!(stmt)

DBInterface.close!(conn)

SQLCatalog, SQLTable, and SQLColumn

In FunSQL, tables and table-like entities are represented using SQLTable objects. Their columns are represented using SQLColumn objects. A collection of SQLTable objects is represented as a SQLCatalog object.

using FunSQL: SQLCatalog, SQLColumn, SQLTable

A SQLTable constructor takes the table name, a vector of columns, and, optionally, the name of the table schema and other qualifiers. A name could be provided either as a Symbol or as a String value. A column can be specified just by its name.

location = SQLTable(qualifiers = [:public],
                    name = :location,
                    columns = [:location_id, :address_1, :address_2,
                               :city, :state, :zip])
#-> SQLTable(qualifiers = [:public], :location, …)

person = SQLTable(name = "person",
                  columns = ["person_id", "year_of_birth", "location_id"])
#-> SQLTable(:person, …)

The table and the column names could be provided as positional arguments.

concept = SQLTable("concept", "concept_id", "concept_name", "vocabulary_id")
#-> SQLTable(:concept, …)

A column may have a custom name for use with FunSQL and the original name for generating SQL queries.

vocabulary = SQLTable(:vocabulary,
                      :id => SQLColumn(:vocabulary_id),
                      :name => SQLColumn(:vocabulary_name))
#-> SQLTable(:vocabulary, …)

A SQLTable object is displayed as a Julia expression that created the object.

display(location)
#=>
SQLTable(qualifiers = [:public],
         :location,
         SQLColumn(:location_id),
         SQLColumn(:address_1),
         SQLColumn(:address_2),
         SQLColumn(:city),
         SQLColumn(:state),
         SQLColumn(:zip))
=#

display(vocabulary)
#=>
SQLTable(:vocabulary,
         :id => SQLColumn(:vocabulary_id),
         :name => SQLColumn(:vocabulary_name))
=#

A SQLTable object behaves like a read-only dictionary.

person[:person_id]
#-> SQLColumn(:person_id)

person["person_id"]
#-> SQLColumn(:person_id)

person[1]
#-> SQLColumn(:person_id)

person[:visit_occurrence]
#-> ERROR: KeyError: key :visit_occurrence not found

get(person, :person_id, nothing)
#-> SQLColumn(:person_id)

get(person, "person_id", nothing)
#-> SQLColumn(:person_id)

get(person, :visit_occurrence, missing)
#-> missing

get(() -> missing, person, :visit_occurrence)
#-> missing

length(person)
#-> 3

collect(keys(person))
#-> [:person_id, :year_of_birth, :location_id]

A SQLCatalog constructor takes a collection of SQLTable objects, the target dialect, and the size of the query cache. Just as columns, a table may have a custom name for use with FunSQL and the original name for generating SQL.

catalog = SQLCatalog(tables = [person, location, concept, :concept_vocabulary => vocabulary],
                     dialect = :sqlite,
                     cache = 128)
#-> SQLCatalog(…4 tables…, dialect = SQLDialect(:sqlite), cache = 128)

display(catalog)
#=>
SQLCatalog(SQLTable(:concept,
                    SQLColumn(:concept_id),
                    SQLColumn(:concept_name),
                    SQLColumn(:vocabulary_id)),
           :concept_vocabulary => SQLTable(:vocabulary,
                                           :id => SQLColumn(:vocabulary_id),
                                           :name => SQLColumn(
                                                        :vocabulary_name)),
           SQLTable(qualifiers = [:public],
                    :location,
                    SQLColumn(:location_id),
                    SQLColumn(:address_1),
                    SQLColumn(:address_2),
                    SQLColumn(:city),
                    SQLColumn(:state),
                    SQLColumn(:zip)),
           SQLTable(:person,
                    SQLColumn(:person_id),
                    SQLColumn(:year_of_birth),
                    SQLColumn(:location_id)),
           dialect = SQLDialect(:sqlite),
           cache = 128)
=#

Number of tables in the catalog affects its representation.

SQLCatalog(tables = [:person => person])
#-> SQLCatalog(…1 table…, dialect = SQLDialect())

SQLCatalog()
#-> SQLCatalog(dialect = SQLDialect())

The query cache can be completely disabled.

cacheless_catalog = SQLCatalog(cache = nothing)
#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)

display(cacheless_catalog)
#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)

Any Dict-like object can serve as a query cache.

customcache_catalog = SQLCatalog(cache = Dict())
#-> SQLCatalog(dialect = SQLDialect(), cache = Dict{Any, Any}())

display(customcache_catalog)
#-> SQLCatalog(dialect = SQLDialect(), cache = (Dict{Any, Any})())

The catalog behaves as a read-only Dict object.

catalog[:person]
#-> SQLTable(:person, …)

catalog["person"]
#-> SQLTable(:person, …)

catalog[:visit_occurrence]
#-> ERROR: KeyError: key :visit_occurrence not found

get(catalog, :person, nothing)
#-> SQLTable(:person, …)

get(catalog, "person", nothing)
#-> SQLTable(:person, …)

get(catalog, :visit_occurrence, missing)
#-> missing

get(() -> missing, catalog, :visit_occurrence)
#-> missing

length(catalog)
#-> 4

sort(collect(keys(catalog)))
#-> [:concept, :concept_vocabulary, :location, :person]

Catalog objects can be assigned arbitrary metadata.

metadata_catalog =
    SQLCatalog(SQLTable(:person,
                        SQLColumn(:person_id, metadata = (; label = "Person ID")),
                        SQLColumn(:year_of_birth, metadata = (;)),
                        metadata = (; caption = "Person", is_view = false)),
               metadata = (; model = "OMOP"))
#-> SQLCatalog(…1 table…, dialect = SQLDialect(), metadata = …)

display(metadata_catalog)
#=>
SQLCatalog(SQLTable(:person,
                    SQLColumn(:person_id, metadata = [:label => "Person ID"]),
                    SQLColumn(:year_of_birth),
                    metadata = [:caption => "Person", :is_view => false]),
           dialect = SQLDialect(),
           metadata = [:model => "OMOP"])
=#

FunSQL metadata supports DataAPI metadata interface.

using DataAPI

DataAPI.metadata(metadata_catalog)
#-> Dict("model" => "OMOP")

DataAPI.metadata(metadata_catalog, style = true)
#-> Dict("model" => ("OMOP", :default))

DataAPI.metadata(metadata_catalog, :name, :default)
#-> :default

DataAPI.metadata(metadata_catalog[:person])["caption"]
#-> "Person"

DataAPI.metadata(metadata_catalog[:person], :is_view, true)
#-> false

DataAPI.colmetadata(metadata_catalog[:person])[:person_id]["label"]
#-> "Person ID"

DataAPI.colmetadata(metadata_catalog[:person], 1, :label)
#-> "Person ID"

DataAPI.colmetadata(metadata_catalog[:person], :year_of_birth, :label, "")
#-> ""

DataAPI.metadata(metadata_catalog[:person][:person_id])
#-> Dict("label" => "Person ID")

DataAPI.metadata(metadata_catalog[:person][:person_id], :label, "")
#-> "Person ID"

SQLDialect

In FunSQL, properties and capabilities of a particular SQL dialect are encapsulated in a SQLDialect object.

using FunSQL: SQLDialect

The desired dialect can be specified by name.

postgresql_dialect = SQLDialect(:postgresql)
#-> SQLDialect(:postgresql)

display(postgresql_dialect)
#-> SQLDialect(:postgresql)

If necessary, the dialect can be customized.

postgresql_odbc_dialect = SQLDialect(:postgresql,
                                     variable_prefix = '?',
                                     variable_style = :positional)
#-> SQLDialect(:postgresql, …)

display(postgresql_odbc_dialect)
#-> SQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)

The default dialect does not correspond to any particular database server.

default_dialect = SQLDialect()
#-> SQLDialect()

display(default_dialect)
#-> SQLDialect()

A completely custom dialect can be specified.

my_dialect = SQLDialect(:my, identifier_quotes = ('<', '>'))
#-> SQLDialect(name = :my, …)

display(my_dialect)
#-> SQLDialect(name = :my, identifier_quotes = ('<', '>'))

SQLString

SQLString represents a serialized SQL query.

using FunSQL: SQLString, pack

sql = SQLString("SELECT * FROM person")
#-> SQLString("SELECT * FROM person")

display(sql)
#-> SQLString("SELECT * FROM person")

SQLString implements the AbstractString interface.

ncodeunits(sql)
#-> 20

codeunit(sql)
#-> UInt8

codeunit(sql, 1)
#-> 0x53

isvalid(sql, 1)
#-> true

join(collect(sql))
#-> "SELECT * FROM person"

print(sql)
#-> SELECT * FROM person

write(IOBuffer(), sql)
#-> 20

String(sql)
#-> "SELECT * FROM person"

SQLString may carry a vector columns describing the output columns of the query.

sql = SQLString("SELECT person_id FROM person", columns = [SQLColumn(:person_id)])
#-> SQLString("SELECT person_id FROM person", columns = […1 column…])

display(sql)
#-> SQLString("SELECT person_id FROM person", columns = [SQLColumn(:person_id)])

When the query has parameters, SQLString should include a vector of parameter names in the order they should appear in DBInterface.execute call.

sql = SQLString("SELECT * FROM person WHERE year_of_birth >= ?", vars = [:YEAR])
#-> SQLString("SELECT * FROM person WHERE year_of_birth >= ?", vars = [:YEAR])

display(sql)
#-> SQLString("SELECT * FROM person WHERE year_of_birth >= ?", vars = [:YEAR])

Function pack converts named parameters to the positional form suitable for use with DBInterface.execute.

pack(sql, (; YEAR = 1950))
#-> Any[1950]

pack(sql, Dict(:YEAR => 1950))
#-> Any[1950]

pack(sql, Dict("YEAR" => 1950))
#-> Any[1950]

pack can also be applied to a regular string, in which case it returns the parameters unchanged.

pack("SELECT * FROM person WHERE year_of_birth >= ?", (1950,))
#-> (1950,)