Package: zillion
Zillion: Make sense of it allIntroductionZillionis a data modeling and analytics tool that allows combining and
analyzing data from multiple datasources through a simple API. It acts as a semantic layer
on top of your data, writes SQL so you don't have to, and easily bolts onto existing
database infrastructure via SQLAlchemy Core. TheZillionNLP extension has experimental
support for AI-powered natural language querying and warehouse configuration.WithZillionyou can:Define a warehouse that contains a variety of SQL and/or file-like
datasourcesDefine or reflect metrics, dimensions, and relationships in your dataRun multi-datasource reports and combine the results in a DataFrameFlexibly aggregate your data with multi-level rollups and table pivotsCustomize or combine fields with formulasApply technical transformations including rolling, cumulative, and rank
statisticsApply automatic type conversions - i.e. get a "year" dimension for free
from a "date" columnSave and share report specificationsUtilize ad hoc or public datasources, tables, and fields to enrich reportsQuery your warehouse with natural language (NLP extension)Leverage AI to bootstrap your warehouse configurations (NLP extension)Table of ContentsInstallationPrimerMetrics and DimensionsWarehouse TheoryQuery LayersWarehouse CreationExecuting ReportsNatural Language QueryingZillion ConfigurationExample - Sales AnalyticsWarehouse ConfigurationReportsAdvanced TopicsSubreportsFormulaMetricsDivisor MetricsDivisor MetricsFormulaDimensionsDataSource FormulasType ConversionsAdHocMetricsAdHocDimensionsAdHocDataTablesTechnicalsConfig VariablesDataSource PrioritySupported DataSourcesMultiprocess ConsiderationsDemo UI / Web APIDocsHow to ContributeInstallationWarning: This project is in an alpha state and is subject to change. Please test carefully for production usage and report any issues.$pipinstallzillion

or

$pipinstallzillion[nlp]PrimerThe following is meant to give a quick overview of some theory and
nomenclature used in data warehousing withZillionwhich will be useful
if you are newer to this area. You can also skip below for a usageexampleor warehouse/datasource creationquickstartoptions.In short:Zillionwrites SQL for you and makes data accessible through a very simple API:result=warehouse.execute(metrics=["revenue","leads"],dimensions=["date"],criteria=[("date",">","2020-01-01"),("partner","=","Partner A")])Metrics and DimensionsInZillionthere are two main types ofFieldsthat will be used in
your report requests:Dimensions: attributes of data used for labelling, grouping, and filteringMetrics: facts and measures that may be broken down along dimensionsAFieldencapsulates the concept of a column in your data. For example, you
may have aFieldcalled "revenue". ThatFieldmay occur across several
datasources or possibly in multiple tables within a single datasource.Zillionunderstands that all of those columns represent the same concept, and it can try
to use any of them to satisfy reports requesting "revenue".Likewise there are two main types of tables used to structure your warehouse:Dimension Tables: reference/attribute tables containing only related
dimensionsMetric Tables: fact tables that may contain metrics and some related
dimensions/attributesDimension tables are often static or slowly growing in terms of row count and contain
attributes tied to a primary key. Some common examples would be lists of US Zip Codes or
company/partner directories.Metric tables are generally more transactional in nature. Some common examples
would be records for web requests, ecommerce sales, or stock market price history.Warehouse TheoryIf you really want to go deep on dimensional modeling and the drill-across
querying techniqueZillionemploys, I recommend reading Ralph Kimball'sbookon data warehousing.To summarize,drill-across
queryingforms one or more queries to satisfy a report request formetricsthat may
exist across multiple datasources and/or tables at a particulardimensiongrain.Zillionsupports flexible warehouse setups such assnowflakeorstarschemas, though it isn't
picky about it. You can specify table relationships through a parent-child
lineage, andZillioncan also infer acceptable joins based on the presence
of dimension table primary keys.Zilliondoes not support many-to-many relationships at this time, though most analytics-focused scenarios should be able to work around that by adding views to the model if needed.Query LayersZillionreports can be thought of as running in two layers:DataSource Layer: SQL queries against the warehouse's datasourcesCombined Layer: A final SQL query against the combined data from the
DataSource LayerThe Combined Layer is just another SQL database (in-memory SQLite by default)
that is used to tie the datasource data together and apply a few additional
features such as rollups, row filters, row limits, sorting, pivots, and technical computations.Warehouse CreationThere are multiple ways to quickly initialize a warehouse from a local or remote file:# Path/link to a CSV, XLSX, XLS, JSON, HTML, or Google Sheet# This builds a single-table Warehouse for quick/ad-hoc analysis.url="https://raw.githubusercontent.com/totalhack/zillion/master/tests/dma_zip.xlsx"wh=Warehouse.from_data_file(url,["Zip_Code"])# Second arg is primary key# Path/link to a sqlite database# This can build a single or multi-table Warehouseurl="https://github.com/totalhack/zillion/blob/master/tests/testdb1?raw=true"wh=Warehouse.from_db_file(url)# Path/link to a WarehouseConfigSchema (or pass a dict)# This is the recommended production approach!config="https://raw.githubusercontent.com/totalhack/zillion/master/examples/example_wh_config.json"wh=Warehouse(config=config)Zillion also provides a helper script to boostrap a DataSource configuration file for an existing database. Seezillion.scripts.bootstrap_datasource_config.py. The bootstrap script requires a connection/database url and output file as arguments. See--helpoutput for more options, including the optional--nlpflag that leverages OpenAI to infer configuration information such as column types, table types, and table relationships. The NLP feature requires the NLP extension to be installed as well as the following set in yourZillionconfig file:OPENAI_MODELOPENAI_API_KEYExecuting ReportsThe main purpose ofZillionis to execute reports against aWarehouse.
At a high level you will be crafting reports as follows:result=warehouse.execute(metrics=["revenue","leads"],dimensions=["date"],criteria=[("date",">","2020-01-01"),("partner","=","Partner A")])print(result.df)# Pandas DataFrameWhen comparing to writing SQL, it's helpful to think of the dimensions as the
target columns of agroup bySQL statement. Think of the metrics as the
columns you areaggregating. Think of the criteria as thewhere
clause. Your criteria are applied in the DataSource Layer SQL queries.TheReportResulthas a Pandas DataFrame with the dimensions as the index and
the metrics as the columns.AReportis said to have agrain, which defines the dimensions each metric
must be able to join to in order to satisfy theReportrequirements. Thegrainis a combination ofalldimensions, including those referenced in
criteria or in metric formulas. In the example above, thegrainwould be{date, partner}. Both "revenue" and "leads" must be able to join to those
dimensions for this report to be possible.These concepts can take time to sink in and obviously vary with the specifics
of your data model, but you will become more familiar with them as you start
putting together reports against your data warehouses.Natural Language QueryingWith the NLP extensionZillionhas experimental support for natural language querying of your data warehouse. For example:result=warehouse.execute_text("revenue and leads by date last month")print(result.df)# Pandas DataFrameThis NLP feature requires a running instance of Qdrant (vector database) and the following values set in yourZillionconfig file:QDRANT_HOSTOPENAI_API_KEYEmbeddings will be produced and stored in both Qdrant and a local cache. The
vector database will be initialized the first time you try to use this by
analyzing all fields in your warehouse. An example docker file to run Qdrant is provided in the root of this repo.You have some control over how fields get embedded. Namely in the configuration for any field you can choose whether to exclude a field from embeddings or override which embeddings map to that field. All fields are
included by default. The following example would exclude thenet_revenuefield from being embedded and maprevenuemetric requests to thegross_revenuefield.{"name":"gross_revenue","type":"numeric(10,2)","aggregation":"sum","rounding":2,"meta":{"nlp":{// enabled defaults to true"embedding_text":"revenue"// str or list of str}}},{"name":"net_revenue","type":"numeric(10,2)","aggregation":"sum","rounding":2,"meta":{"nlp":{"enabled":false}}},Additionally you may also exclude fields via the following warehouse-level configuration settings:{"meta":{"nlp":{"field_disabled_patterns":[// list of regex patterns to exclude"rpl_ma_5"],"field_disabled_groups":[// list of "groups" to exclude, assuming you have// set group value in the field's meta dict."No NLP"]}},...}If a field is disabled at any of the aforementioned levels it will be ignored. This type of control becomes useful as your data model gets more complex and you want to guide the NLP logic in cases where it could confuse similarly named fields. Any time you adjust which fields are excluded you will want to force recreation of your embeddings collection using theforce_recreateflag onWarehouse.init_embeddings.Note:This feature is in its infancy. It's usefulness will depend on the
quality of both the input query and your data model (i.e. good field names)!Zillion ConfigurationIn addition to configuring the structure of yourWarehouse, which will be
discussed further below,Zillionhas a global configuration to control some
basic settings. TheZILLION_CONFIGenvironment var can point to a yaml config file. Seeexamples/sample_config.yamlfor more details on what values can be set. Environment vars prefixed with ZILLION_ can override config settings (i.e. ZILLION_DB_URL will override DB_URL).The database used to store Zillion report specs can be configured by setting the DB_URL value in yourZillionconfig to a valid database connection string. By default a SQLite DB in /tmp is used.Example - Sales AnalyticsBelow we will walk through a simple hypothetical sales data model that
demonstrates basicDataSourceandWarehouseconfiguration and then shows
some samplereports. The data is a simple SQLite database
that is part of theZilliontest code. For reference, the schema is as
follows:CREATETABLEpartners(idINTEGERPRIMARYKEY,nameVARCHARNOTNULLUNIQUE,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);CREATETABLEcampaigns(idINTEGERPRIMARYKEY,nameVARCHARNOTNULLUNIQUE,categoryVARCHARNOTNULL,partner_idINTEGERNOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);CREATETABLEleads(idINTEGERPRIMARYKEY,nameVARCHARNOTNULL,campaign_idINTEGERNOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);CREATETABLEsales(idINTEGERPRIMARYKEY,itemVARCHARNOTNULL,quantityINTEGERNOTNULL,revenueDECIMAL(10,2),lead_idINTEGERNOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);Warehouse ConfigurationAWarehousemay be created from a JSON or YAML configuration that defines
its fields, datasources, and tables. The code below shows how it can be done in as little as one line of code if you have a pointer to a JSON/YAMLWarehouseconfig.fromzillionimportWarehousewh=Warehouse(config="https://raw.githubusercontent.com/totalhack/zillion/master/examples/example_wh_config.json")This example config uses adata_urlin itsDataSourceconnectinfo that
tellsZillionto dynamically download that data and connect to it as a
SQLite database. This is useful for quick examples or analysis, though in most
scenarios you would put a connection string to an existing database like you
seehereThe basics ofZillion'swarehouse configuration structure are as follows:AWarehouseconfig has the following main sections:metrics: optional list of metric configs for global metricsdimensions: optional list of dimension configs for global dimensionsdatasources: mapping of datasource names to datasource configs or config URLsADataSourceconfig has the following main sections:connect: database connection url or dict of connect paramsmetrics: optional list of metric configs specific to this datasourcedimensions: optional list of dimension configs specific to this datasourcetables: mapping of table names to table configs or config URLsTip: datasource and table configs may also be replaced with a URL that points
to a local or remote config file.In this example all four tables in our database are included in the config,
two as dimension tables and two as metric tables. The tables are linked
through a parent->child relationship: partners to campaigns, and leads to
sales.  Some tables also utilize thecreate_fieldsflag to automatically
createFieldson the datasource from column definitions. Other metrics and
dimensions are defined explicitly.To view the structure of thisWarehouseafter init you can use theprint_infomethod which shows all metrics, dimensions, tables, and columns that are part
of your data warehouse:wh.print_info()# Formatted print of the Warehouse structureFor a deeper dive of the config schema please see the fulldocs.ReportsExample:Get sales, leads, and revenue by partner:result=wh.execute(metrics=["sales","leads","revenue"],dimensions=["partner_name"])print(result.df)"""sales  leads  revenuepartner_namePartner A        11      4    165.0Partner B         2      2     19.0Partner C         5      1    118.5"""Example:Let's limit to Partner A and break down by its campaigns:result=wh.execute(metrics=["sales","leads","revenue"],dimensions=["campaign_name"],criteria=[("partner_name","=","Partner A")])print(result.df)"""sales  leads  revenuecampaign_nameCampaign 1A        5      2       83Campaign 2A        6      2       82"""Example:The output below shows rollups at the campaign level within each
partner, and also a rollup of totals at the partner and campaign level.Note:the output contains a special character to mark DataFrame rollup rows
that were added to the result. TheReportResultobject contains some helper attributes to automatically access or filter
rollups, as well as adf_displayattribute that returns the result with
friendlier display values substituted for special characters. The
under-the-hood special character is left here for illustration, but may not
render the same in all scenarios.fromzillionimportRollupTypesresult=wh.execute(metrics=["sales","leads","revenue"],dimensions=["partner_name","campaign_name"],rollup=RollupTypes.ALL)print(result.df)"""sales  leads  revenuepartner_name campaign_namePartner A    Campaign 1A      5.0    2.0     83.0Campaign 2A      6.0    2.0     82.0􏿿               11.0    4.0    165.0Partner B    Campaign 1B      1.0    1.0      6.0Campaign 2B      1.0    1.0     13.0􏿿                2.0    2.0     19.0Partner C    Campaign 1C      5.0    1.0    118.5􏿿                5.0    1.0    118.5􏿿            􏿿               18.0    7.0    302.5"""See theReportdocsfor more
information on supported rollup behavior.Example:Save a report spec (not the data):First you must make sure you have saved yourWarehouse, as saved reports
are scoped to a particularWarehouseID. To save aWarehouseyou must provide a URL that points to the complete config.name="My Unique Warehouse Name"config_url=<someurlpointingtoacompletewarehouseconfig>wh.save(name,config_url)# wh.id is populated after thisspec_id=wh.save_report(metrics=["sales","leads","revenue"],dimensions=["partner_name"])Note: If you built yourWarehousein python from a list ofDataSources,
or passed in adictfor theconfigparam on init, there currently is not
a built-in way to output a complete config to a file for reference when saving.Example:Load and run a report from a spec ID:result=wh.execute_id(spec_id)This assumes you have saved this report ID previously in the database specified by the DB_URL in yourZillionyaml configuration.Example:Unsupported GrainIf you attempt an impossible report, you will get anUnsupportedGrainException. The report below is impossible because it
attempts to break down the leads metric by a dimension that only exists
in a child table. Generally speaking, child tables can join back up to
parents (and "siblings" of parents) to find dimensions, but not the other
way around.# Fails with UnsupportedGrainExceptionresult=wh.execute(metrics=["leads"],dimensions=["sale_id"])Advanced TopicsSubreportsSometimes you need subquery-like functionality in order to filter one
report to the results of some other (that perhaps required a different grain).
Zillion provides a simplistic way of doing that by using thein reportornot in reportcriteria operations. There are two supported ways to specify the subreport: passing a
report spec ID or passing a dict of report params.# Assuming you have saved report 1234 and it has "partner" as a dimension:result=warehouse.execute(metrics=["revenue","leads"],dimensions=["date"],criteria=[("date",">","2020-01-01"),("partner","in report",1234)])# Or with a dict:result=warehouse.execute(metrics=["revenue","leads"],dimensions=["date"],criteria=[("date",">","2020-01-01"),("partner","in report",dict(metrics=[...],dimension=["partner"],criteria=[...]))])The criteria field used inin reportornot in reportmust be a dimension
in the subreport. Note that subreports are executed atReportobject initialization
time instead of duringexecute-- as such they can not be killed usingReport.kill.
This may change down the road.Formula MetricsIn our example above our config included a formula-based metric called "rpl",
which is simplyrevenue / leads. AFormulaMetriccombines other metrics
and/or dimensions to calculate a new metric at the Combined Layer of
querying. The syntax must match your Combined Layer database, which is SQLite
in our example.{"name":"rpl","aggregation":"mean","rounding":2,"formula":"{revenue}/{leads}"}Divisor MetricsAs a convenience, rather than having to repeatedly define formula metrics for
rate variants of a core metric, you can specify a divisor metric configuration on a non-formula metric. As an example, say you have arevenuemetric and want to create variants forrevenue_per_leadandrevenue_per_sale. You can define your revenue metric as follows:{"name":"revenue","type":"numeric(10,2)","aggregation":"sum","rounding":2,"divisors":{"metrics":["leads","sales"]}}Seezillion.configs.DivisorsConfigSchemafor more details on configuration options, such as overriding naming templates, formula templates, and rounding.Aggregation VariantsAnother minor convenience feature is the ability to automatically generate variants of metrics for different aggregation types in a single field configuration instead of across multiple fields in your config file. As an example, say you have asalescolumn in your data and want to create variants forsales_meanandsales_sum. You can define your metric as follows:{"name":"sales","aggregation":{"mean":{"type":"numeric(10,2)","rounding":2},"sum":{"type":"integer"}}}The final config would not have asalesmetric, but would instead havesales_meanandsales_sum. Note that you can further customize the settings for the generated fields, such as getting a custom name, by specifying that in the nested settings for that aggregation type. In practice it's not a big savings over just defining the metrics separately, but some may prefer this approach.Formula DimensionsExperimental support exists forFormulaDimensionfields as well. AFormulaDimensioncan only use other dimensions as part of its formula, and it also gets evaluated in the Combined Layer database. As an additional restriction, aFormulaDimensioncan not be used in report criteria as those filters are evaluated at the DataSource Layer. The following example assumes a SQLite Combined Layer database:{"name":"partner_is_a","formula":"{partner_name} = 'Partner A'"}DataSource FormulasOur example also includes a metric "sales" whose value is calculated via
formula at the DataSource Layer of querying. Note the following in thefieldslist for the "id" param in the "main.sales" table. These formulas are
in the syntax of the particularDataSourcedatabase technology, which also
happens to be SQLite in our example."fields":["sale_id",{"name":"sales","ds_formula":"COUNT(DISTINCT sales.id)"}]Type ConversionsOur example also automatically created a handful of dimensions from the
"created_at" columns of the leads and sales tables. Support for automatic type
conversions is limited, but for date/datetime columns in supportedDataSourcetechnologies you can get a variety of dimensions for free this
way.The output ofwh.print_infowill show the added dimensions, which are
prefixed with "lead_" or "sale_" as specified by the optionaltype_conversion_prefixin the config for each table. Some examples of
auto-generated dimensions in our example warehouse include sale_hour,
sale_day_name, sale_month, sale_year, etc.As an optimization in the where clause of underlying report queries,Zillionwill try to apply conversions to criteria values instead of columns. For example,
it is generally more efficient to query asmy_datetime > '2020-01-01' and my_datetime < '2020-01-02'instead ofDATE(my_datetime) == '2020-01-01', because the latter can prevent index
usage in many database technologies. The ability to apply conversions to values
instead of columns varies by field andDataSourcetechnology as well.To prevent type conversions, setskip_conversion_fieldstotrueon yourDataSourceconfig.Seezillion.field.TYPE_ALLOWED_CONVERSIONSandzillion.field.DIALECT_CONVERSIONSfor more details on currently supported conversions.Ad Hoc MetricsYou may also define metrics "ad hoc" with each report request. Below is an
example that creates a revenue-per-lead metric on the fly. These only exist
within the scope of the report, and the name can not conflict with any existing
fields:result=wh.execute(metrics=["leads",{"formula":"{revenue}/{leads}","name":"my_rpl"}],dimensions=["partner_name"])Ad Hoc DimensionsYou may also define dimensions "ad hoc" with each report request. Below is an
example that creates a dimension that partitions on a particular dimension value on the fly. Ad Hoc Dimensions are a subclass ofFormulaDimensions and therefore have the same restrictions, such as not being able to use a metric as a formula field. These only exist within the scope of the report, and the name can not conflict with any existing fields:result=wh.execute(metrics=["leads"],dimensions=[{"name":"partner_is_a","formula":"{partner_name}= 'Partner A'"])Ad Hoc TablesZillionalso supports creation or syncing of ad hoc tables in your database
duringDataSourceorWarehouseinit. An example of a table config that
does this is shownhere.
It uses the table config'sdata_urlandif_existsparams to control the
syncing and/or creation of the "main.dma_zip" table from a remote CSV in a
SQLite database.  The same can be done in other database types too.The potential performance drawbacks to such an approach should be obvious,
particularly if you are initializing your warehouse often or if the remote
data file is large. It is often better to sync and create your data ahead of
time so you have complete schema control, but this method can be very useful
in certain scenarios.Warning: be careful not to overwrite existing tables in your database!TechnicalsThere are a variety of technical computations that can be applied to metrics to
compute rolling, cumulative, or rank statistics. For example, to compute a 5-point
moving average on revenue one might define a new metric as follows:{"name":"revenue_ma_5","type":"numeric(10,2)","aggregation":"sum","rounding":2,"technical":"mean(5)"}Technical computations are computed at the Combined Layer, whereas the "aggregation"
is done at the DataSource Layer (hence needing to define both above).For more info on how shorthand technical strings are parsed, see theparse_technical_stringcode. For a full list of supported technical types seezillion.core.TechnicalTypes.Technicals also support two modes: "group" and "all". The mode controls how to
apply the technical computation across the data's dimensions. In "group" mode,
it computes the technical across the last dimension, whereas in "all" mode in
computes the technical across all data without any regard for dimensions.The point of this becomes more clear if you try to do a "cumsum" technical
across data broken down by something like ["partner_name", "date"]. If "group"
mode is used (the default in most cases) it will do cumulative sumswithineach partner over the date ranges. If "all" mode is used, it will do a
cumulative sum across every data row. You can be explicit about the mode by
appending it to the technical string: i.e. "cumsum:all" or "mean(5):group"Config VariablesIf you'd like to avoid putting sensitive connection information directly in
yourDataSourceconfigs you can leverage config variables. In yourZillionyaml config you can specify aDATASOURCE_CONTEXTSsection as follows:DATASOURCE_CONTEXTS:my_ds_name:user:user123pass:goodpasswordhost:127.0.0.1schema:reportingThen when yourDataSourceconfig for the datasource named "my_ds_name" is
read, it can use this context to populate variables in your connection url:"datasources":{"my_ds_name":{"connect":"mysql+pymysql://{user}:{pass}@{host}/{schema}"...}}DataSource PriorityOnWarehouseinit you can specify a default priority order for datasources
by name. This will come into play when a report could be satisfied by multiple
datasources.DataSourcesearlier in the list will be higher priority. This
would be useful if you wanted to favor a set of faster, aggregate tables that
are grouped in aDataSource.wh=Warehouse(config=config,ds_priority=["aggr_ds","raw_ds",...])Supported DataSourcesZillion'sgoal is to support any database technology that SQLAlchemy
supports (pictured below). That said the support and testing levels inZillionvary at the
moment. In particular, the ability to do type conversions, database
reflection, and kill running queries all require some database-specific code
for support. The following list summarizes known support levels. Your mileage
may vary with untested database technologies that SQLAlchemy supports (it
might work just fine, just hasn't been tested yet). Please report bugs and
help add more support!SQLite: supportedMySQL: supportedPostgreSQL: supportedDuckDB: supportedBigQuery, Redshift, Snowflake, SingleStore, PlanetScale, etc: not tested but would like to support theseSQLAlchemy has connectors to many popular databases. The barrier to support many of these is likely
pretty low given the simple nature of the sql operationsZillionuses.Note that the above is different than the database support for the Combined Layer
database. Currently only SQLite is supported there; that should be sufficient for
most use cases but more options will be added down the road.Multiprocess ConsiderationsIf you plan to runZillionin a multiprocess scenario, whether on a single
node or across multiple nodes, there are a couple of things to consider:SQLite DataSources do not scale well and may run into locking issues with multiple processes trying to access them on the same node.Any file-based database technology that isn't centrally accessible would be challenging when using multiple nodes.Ad Hoc DataSource and Ad Hoc Table downloads should be avoided as they may conflict/repeat across each process. Offload this to an external
ETL process that is better suited to manage those data flows in a scalable production scenario.Note that you can still use the default SQLite in-memory Combined Layer DB without issues, as that is made on the fly with each report request and
requires no coordination/communication with other processes or nodes.Demo UI / Web APIZillion Web UIis a demo UI and web API for Zillion that also includes an experimental ChatGPT plugin. See the README there for more info on installation and project structure. Please note that the code is light on testing and polish, but is expected to work in modern browsers. Also ChatGPT plugins are quite slow at the moment, so currently that is mostly for fun and not that useful.DocumentationMore thorough documentation can be foundhere.
You can supplement your knowledge by perusing thetestsdirectory
or theAPI reference.How to ContributePlease See thecontributingguide for more information. If you are looking for inspiration, adding support and tests for additional database technologies would be a great help.