Synthetic data¶
A synthetic data source serves generated rows that look like the rows of another data source. Use it to give developers and tests realistic data without real values.
Sluice reads the source once, builds a profile of each column, and writes generated rows to a snapshot file. Clients query the snapshot like any other catalog.
Example¶
Goal: let developers query customers and orders without access to the real rows.
The shop data source holds 400 customers and 2000 orders. shopsynth builds 20% of each table from the profile of shop:
apiVersion: sluice.bino.bi/v1alpha1
kind: DataSource
metadata:
name: shopsynth
spec:
type: synthetic
synthetic:
source: shop
seed: secret://env/SHOP_SYNTH_SEED
privacy:
minGroupSize: 5
tables:
- name: main.orders
size: { ratio: 0.2 }
columns:
id: { generator: sequence }
customer_id:
generator: foreignKey
args: { table: main.customers, column: id }
product: { generator: categorical }
amount:
generator: correlated
args: { with: [quantity], decimals: 2 }
quantity: { generator: correlated }
placed_on:
generator: dateDistribution
args: { by: [product] }
shipped_on:
generator: dateOffset
args: { from: placed_on }
- name: main.customers
size: { ratio: 0.2 }
privacy:
rejectExactMatches: [first_name, last_name]
columns:
id: { generator: sequence }
first_name:
generator: fake
args: { kind: first_name, keepInitial: true }
last_name:
generator: fake
args: { kind: last_name, keepInitial: true }
email:
generator: template
args: { template: "{first_name}.{last_name}@example.org", lower: true }
customer_idtakes the keys of the synthetic customers. Each customer gets about as many orders as in the source.amountandquantitykeep their rank correlation. See Correlated columns.placed_onfollows the dates of the same product in the source.shipped_onisplaced_onplus a gap like the source gaps.emailis built from the synthetic names of the same row.- No synthetic customer has the first and last name of a source customer.
The seed is a secret reference to at least 16 bytes. A DataClassification tags email as pii. A tagged column needs a column entry or a rule, so the template entry is required:
apiVersion: sluice.bino.bi/v1beta1
kind: DataClassification
metadata:
name: shop-tags
spec:
rules:
- resources:
catalogs: ["shop"]
tables: ["customers"]
columns: ["email"]
tags: ["pii"]
The server config names the folder of the snapshots:
Build the snapshot, then start the server:
An access policy allows the group developers to read shopsynth. Another policy denies shop. A developer counts the orders per product:
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_dana.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT product, count(*) AS orders FROM shopsynth.main.orders GROUP BY product ORDER BY product"}'
{
"query_id": "<query-id>",
"columns": ["product", "orders"],
"rows": [
["coffee", 180],
["mug", 63],
["tea", 157]
],
"row_count": 3,
"truncated": false
}
The same query on shop is denied:
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_dana.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT product, count(*) AS orders FROM shop.main.orders GROUP BY product ORDER BY product"}'
{
"code": "ACL_DENIED",
"message": "access denied",
"query_id": "<query-id>",
"policy": "deny-shop"
}
What happened: the build profiled shop.main.customers and shop.main.orders and wrote 80 customers and 400 orders. The product gift card has 2 source rows, below minGroupSize, so it never appears. The shares of the other products stay close to the source.
Fields¶
The policy schema lists every field of synthetic. A table entry needs a name as schema.table and a size: either ratio or rows.
Each column gets one generator. Sluice takes the first match in this order:
- The entry of the column under
tables[].columns. - The first matching rule under
tables[].rules. - The first matching rule under
synthetic.rules. - The default for the column type.
A rule matches by columns (name patterns), types (type patterns such as DECIMAL*) and tags. All fields that are set must match.
| Column type | Default generator |
|---|---|
Integers with at most 50 distinct values, each in at least minGroupSize rows | categorical |
Other numbers, TIME | distribution |
BOOLEAN | boolean |
ENUM | categorical |
VARCHAR with at most 50 distinct values, each in at least minGroupSize rows | categorical |
Other VARCHAR | randomString with length: source |
DATE, TIMESTAMP types | dateDistribution |
UUID | uuid |
Other types, such as BLOB, lists and structs | null, with a report warning |
A default column with fewer than minGroupSize non-null rows is written as NULL, with a report warning. A column entry for a column that the source table does not have fails the build.
Generators¶
source means "from the profile of the source column". Every generator also takes nullRate and unique:
nullRate:source(default) or a share from 0 to 1. Fortemplateandexpressionthe default is 0.unique: true: no value repeats in the column. The build fails when the generator runs out of values.template,expression,foreignKeyandcorrelateddo not take it.
Number types are the integer types, DECIMAL, FLOAT and DOUBLE. Text is VARCHAR.
| Generator | Column types | Args, default in parentheses | Values |
|---|---|---|---|
distribution | Numbers, TIME | decimals, integer, smooth (true) | Follows the quantiles of the source. |
correlated | Numbers, TIME | with, decimals, integer, smooth (true) | Keeps the rank correlation between the columns of a group. See Correlated columns. |
uniform | Numbers, TIME | min, max (source bounds) | Even spread between the bounds. |
normal | Numbers | truncate (true), decimals, integer | Normal curve with the source mean and deviation. |
lognormal | Numbers | truncate (true), decimals, integer | Log-normal curve. The source must hold only positive values. |
histogram | Numbers | bins (20), decimals, integer | Equal-width bins with the source counts. |
bucket | Numbers | edges or buckets (5) | Range labels such as 30-39. Written as VARCHAR. |
constant | Any except BLOB, lists and structs | value, required | One fixed value. |
sequence | Integers, text | start (1), step (1) | 1, 2, 3 and so on. |
range | Numbers | min, max, both required, decimals | Even spread between min and max. |
categorical | Any except BLOB, lists and structs | maxValues (50), otherLabel | Source values with their shares. |
uniformCategory | Any except BLOB, lists and structs | None | Source values, all with the same share. |
weighted | Any except BLOB, lists and structs | values, required: a map of value to weight | Your values with your weights. |
boolean | BOOLEAN | trueRate (source) | true with the given share. |
shuffleLabels | Any except BLOB, lists and structs | maxValues (50) | Source values with the shares of other values. |
randomString | Text | length, initial, charset, case | Random letters with the source lengths. |
pattern | Text | maxPatterns (20) | Source shapes: DE-4711 gives QK-8302. |
regex | Text | pattern, required | Text that matches the pattern. |
fake | Text | kind, required, locale (en), keepInitial | Fake names, emails, phone numbers, cities, IBANs and more. |
uuid | UUID, text | version (4) | Random UUIDs, version 4 or 7. |
prefixKeep | Text | prefixLength, required | Source prefixes, random rest. |
loremText | Text | words (source), vocabulary (lorem) | Filler words with the source word counts. |
randomText | Text | length (source), vocabulary (lorem) | Filler words cut to the source lengths. |
redact | Text | value ([redacted]) | One fixed text. |
dateDistribution | DATE, TIMESTAMP types | granularity, keepWeekday, keepSeasonality | Follows the dates of the source. |
dateRange | DATE, TIMESTAMP types | min, max (source bounds), granularity | Even spread between the bounds. |
age | DATE, TIMESTAMP types | asOf (upper source bound), granularity | Dates with the source ages at asOf, such as birth dates. See Ages. |
timeOfDay | TIME, TIMESTAMP types | None | Keeps the times of day, such as office hours. See Times of day. |
null | Any | None | Always NULL. |
foreignKey | Integers, text, UUID | table and column, both required, fanOut (source), orphanRate (0) | Keys of a parent table. See Foreign keys. |
dateOffset | DATE, TIMESTAMP types | from, required, offset (source) | The value of from plus a gap like the source gaps. |
template | Text | template, required, lower (false) | Text from other columns of the row, such as {first_name}.{last_name}. |
expression | Any except BLOB, INTERVAL, lists and structs | sql, required | A SQL expression over other columns of the row. See Expressions. |
sourceVocabulary | Text | minWordCount (20), words (source), maxWords (1000) | Frequent source words with their shares. |
sentenceShape | Text | vocabulary (lorem), minWordCount, maxWords | Sentences with the source counts, lengths and end marks. |
distribution,categoricalanddateDistributionalso takeby. See Groups with by.lengthandwordstakesource, a number, or{min, max}.fanOuttakessourceor{min, max}: the number of child rows per parent, from 0 to 100000.orphanRatetakessourceor a share from 0 to 1. It is the share of child rows with a key that has no parent.offsettakessourceor{min, max}in days, from -36500 to 36500.- In a
template,{name}inserts a column of the row. Write{{and}}for a brace.lower: truewrites the result in lower case. sourceVocabularykeeps a word only when it occursminWordCounttimes and inminGroupSizerows. A word is a run of letters, so digits never appear.maxWordsis the size of the word list.sentenceShape.vocabularyis one oflorem,en,de,source.minWordCountandmaxWordswork only withsource.fake.kindis one ofname,first_name,last_name,email,phone,company,city,country,street,iban,uuid.granularityis one ofsecond,minute,hour,day,month,year. ADATEcolumn takes onlyday,monthandyear.- Quote the keys
"Y"and"N"inweighted.values. YAML reads them as booleans otherwise. - A generator that does not fit the column type fails the build before Sluice profiles the column.
Ages¶
age keeps the age mix of a birth date column. Sluice counts the source rows per age in whole years at the date asOf. For each row it draws an age with these shares and writes a date in that year of life before asOf. For example: birth_date: { generator: age, args: { asOf: "2024-06-30" } }.
asOfis an ISO date or timestamp. Without it,asOfis the upper bound of the column afterclipOutliersandextremeJitter. Set it to the date of the source data, so the ages mean something.- An age with fewer than
minGroupSizerows never appears. Its rows are left out of the shares. clipOutlierscuts the dates before Sluice counts the ages. A spike at the lowest or highest date gives a warning.- A date after
asOfcounts as age 0. An age above 150 counts as 150. granularitycuts the written date. The cut date keeps the drawn age.- With
noiseandboundson the column,asOfdefaults tobounds.max, and dates outside the bounds count as the nearest bound.
Times of day¶
timeOfDay keeps the times of day of a column. Sluice counts the source rows per 15 minutes of the day. Each row gets a slot with these shares and a second in that slot.
- A slot with fewer than
minGroupSizerows never appears. Its rows are left out of the shares. - On a
TIMESTAMPcolumn the date comes from the source dates, as withdateDistributionandgranularity: day. Then the time of day is added. A value past the range on the first or last day moves one day into the range. - On a
TIMEcolumn the times have whole seconds.
Correlated columns¶
correlated keeps the rank correlation between number columns of one table. In the example source, a larger amount comes with a larger quantity:
amount:
generator: correlated
args: { with: [quantity], decimals: 2 }
quantity: { generator: correlated }
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_dana.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT round(corr(amount, quantity), 1) AS correlation FROM shopsynth.main.orders"}'
{
"query_id": "<query-id>",
"columns": ["correlation"],
"rows": [
[0.9]
],
"row_count": 1,
"truncated": false
}
What happened: each column got its own values, as with distribution. The group only decided which values meet in one row.
withnames 1 to 7 othercorrelatedcolumns of the same table entry. One side is enough:quantityneeds nowith.- Columns that
withlinks form a group of 2 to 8 columns. - Sluice measures the rank correlation (Spearman) of each pair from a table of up to 20 by 20 cells.
- A pair needs
2 x minGroupSizerows where both values are set. Fewer rows fail the build. - A pair without spread, such as a constant column, gets the correlation 0 and a report warning.
- When the measured values do not fit together, Sluice moves them toward 0 until they do. The report shows this share as
shrinkage. correlatedis set only undertables[].columns, not in a rule.DATEandTIMESTAMPcolumns are refused.correlateddoes not takeunique, andrejectExactMatchescannot name it. Sluice cannot draw one row of a group again.
Privacy settings¶
Set privacy on the data source, on a table, or on a column entry. The most specific level wins.
| Setting | Default | What it does |
|---|---|---|
minGroupSize | 5 | A value, length, bin or bound must come from at least this many source rows. Rarer values never appear. |
clipOutliers | Off | Cuts the source below the lower and above the upper quantile before the profile. |
extremeJitter | {min: 0.01, max: 0.02} | Moves the lowest and highest value by a random share of the range. off turns it off. |
spikeWarning | true | Warns when fewer than minGroupSize rows lie in the lowest or highest 1% of a number or date range. |
strictTags | [pii] | A column with one of these tags needs a column entry or a rule. The default generator is not enough. |
rejectExactMatches | Off | Columns that together must not equal a source row. Sluice draws matching rows again. |
noise | Off | Adds random noise to the counts of the profile. Takes epsilon from 0.001 to 100. See Noise. |
bounds | None | The public min and max of a number, TIME or date column. Needs noise. |
- Columns with fewer than
minGroupSizenon-null rows fail the build, unless the generator needs no profile. Examples:constant,sequence,range,weighted,regex,uuid,redact,null. - A spike goes to the build log, the report and the metric
sluice_synthetic_spikes. The build does not fail. SetclipOutliersto remove it. strictTagsis set only on the data source. Tags come fromDataClassificationobjects on the source catalog.rejectExactMatchesis set only on a table. It takes 1 to 32 column names.noiseis set only on the data source.boundsis set only on a column entry.
rejectExactMatches compares the listed columns of each synthetic row with every source row:
- Values compare in the snapshot types.
NULLequalsNULL. - The build fails when more than 1% of the rows match. Then the columns are too predictable together.
- Otherwise Sluice draws the listed columns of the matching rows again, and the columns that read them. It checks again, up to 10 rounds. Rows that still match fail the build.
- Source rows stay inside DuckDB. Errors and the report hold only counts and column names.
Noise¶
noise adds random noise (Laplace noise) to the counts that Sluice reads from the source. One source row then changes the snapshot only a little. A smaller epsilon means more noise.
shopnoisy builds the orders of the example with noise. amount gets public bounds:
apiVersion: sluice.bino.bi/v1alpha1
kind: DataSource
metadata:
name: shopnoisy
spec:
type: synthetic
synthetic:
source: shop
seed: secret://env/SHOP_SYNTH_SEED
privacy:
minGroupSize: 5
noise: { epsilon: 10 }
tables:
- name: main.orders
size: { ratio: 0.2 }
columns:
product: { generator: categorical }
amount:
generator: histogram
args: { bins: 20, decimals: 2 }
privacy:
bounds: { min: 0, max: 260 }
build <build-id> succeeded, origin cli, finished <time>
noise: epsilon 10, 22 measurements
TABLE COLUMN GENERATOR SOURCE MEDIAN SYNTHETIC MEDIAN DISTANCE WARNINGS
main.orders id distribution 1000.5 1029 ks 0.0701 1
main.orders customer_id distribution 189.5 151 ks 0.023 1
main.orders region categorical - - tv 0.0222 0
main.orders product categorical - - tv 0.0213 0
main.orders amount histogram 67.375 71.99 - 0
main.orders quantity categorical 5.5 5 tv 0.0649 0
main.orders placed_on dateDistribution 20602.5 20591 ks 0.0323 1
main.orders shipped_on dateDistribution 20607.5 20611.5 ks 0.0292 1
warning: main.orders.id: range is not covered by noise, set privacy.bounds
warning: main.orders.customer_id: range is not covered by noise, set privacy.bounds
warning: main.orders.placed_on: range is not covered by noise, set privacy.bounds
warning: main.orders.shipped_on: range is not covered by noise, set privacy.bounds
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_dana.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT round(min(amount)) AS min_amount, round(max(amount)) AS max_amount FROM shopnoisy.main.orders"}'
{
"query_id": "<query-id>",
"columns": ["min_amount", "max_amount"],
"rows": [
[0, 259]
],
"row_count": 1,
"truncated": false
}
What happened: the build made 22 noisy measurements, so each got an epsilon of 10 / 22. The range of amount comes from its bounds and the kept bins. The other number and date columns warn that their range has no noise.
Budget¶
epsilonis the budget of one build of the whole data source. Sluice splits it evenly over all noisy measurements of the build.- A measurement is one released set of counts. Examples: the rows of a table, the nulls of a column, one histogram, one correlated pair.
- A rebuild over changed data spends
epsilonagain. Nothing adds up the budget over builds. - Sluice counts the measurements before it reads data. A default with a fallback counts every generator it may fall back to.
foreignKeywithfanOut: sourceororphanRate: source,dateOffset,sourceVocabulary,sentenceShapeandbyare refused withnoise.
Counts¶
- Each count is the true count plus noise, rounded, and at least 0.
- A histogram cell is kept when it reaches
minGroupSizeand a floor that grows with the noise. A cell is a bin, a weekday, an age or a time slot. - A value, initial, pattern or prefix is kept when its noisy count reaches
minGroupSize. - Quantiles, mean and deviation come from a noisy histogram of 200 bins, or of
binsforhistogram. - The same data, seed, spec and DuckDB version give the same rows. Any change in the source changes the noise of every count.
Min and max¶
- With
bounds, Sluice cuts the values to the bounds and splits that range into equal bins. The output range is the outer edges of the kept bins. - A lone far value cannot keep its bin, so it does not show in the range.
- Set
boundsclose to the real range. A bin is (max - min) / 200 wide, so wide bounds put all rows in a few bins. - On small tables use
histogramwith fewerbins. The example has 2000 rows and needsepsilon: 10for usable numbers. - Without
bounds, the range comes from the profile, as without noise. The report marks the columnrange: profileand warns. boundstakes numbers for number columns and seconds of the day forTIME. ForDATEandTIMESTAMPcolumns it takes ISO dates or timestamps.boundson a generator that reads no numbers, such ascategorical, fails the build.- A very small
epsiloncan fail the build for a number column. It can also give the column a narrow random range.
Not covered by noise¶
Do not call a snapshot differentially private without this list. Noise does not cover:
- Which values, initials, patterns and prefixes appear.
minGroupSizeprotects them, as without noise. - The range of number and date columns without
bounds, and spike warnings withoutbounds. - The report and the audit record. They hold exact counts of the source.
- Whether the source changed between two builds.
- The rows that
rejectExactMatchescompares with the source. - Attacks on the rounding of floating-point numbers in the noise.
- A known seed. Anyone with the seed and the spec can remove the noise.
Relations¶
foreignKey, dateOffset, template, expression and by read other columns. Set them only under tables[].columns. A rule matches columns in many tables, so a column name in its args could point to a different column in each.
Every synthetic order of the example points to a synthetic customer. Count the orders per city through a join:
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_dana.world" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT c.city, count(*) AS orders FROM shopsynth.main.orders o LEFT JOIN shopsynth.main.customers c ON c.id = o.customer_id GROUP BY c.city ORDER BY c.city NULLS LAST"}'
{
"query_id": "<query-id>",
"columns": ["city", "orders"],
"rows": [
["Bremen", 97],
["Essen", 72],
["Hamburg", 105],
["Kiel", 126]
],
"row_count": 4,
"truncated": false
}
What happened: all 400 orders find their customer, so no row has the city NULL.
Order¶
- Sluice builds a parent table before the tables that point to it. Other tables keep the list order. The report lists the tables in build order.
- Inside a row, Sluice draws a column after the columns it reads.
- A
foreignKeyto its own table is refused. Tables or columns that depend on each other in a cycle are refused. sluice config validatefinds these errors. A column name that the source table does not have fails the build.
Foreign keys¶
- The parent column needs unique values: the generator
sequenceoruuid, orunique: true. - Parent and child column hold the same kind of key: both integers, or both text or
UUID. fanOut: sourcekeeps the fan-out of the source: the number of child rows per parent. A fan-out that fewer thanminGroupSizeparents share is merged into a near one.- When the table sizes do not fit the source fan-out, Sluice scales it. The report warns when the scale is below 0.5 or above 2.
- Orphan keys are new values, not source keys.
- After the write, Sluice checks that every key except the orphans finds its parent. The snapshot has no
FOREIGN KEYconstraint. - Sluice keeps the parent keys in memory during the build, about 50 bytes per parent row.
Groups with by¶
by names 1 to 3 columns. The generator then uses the profile of the rows that have the same values in these columns. The example uses the dates of the same product.
- A
bycolumn is text,ENUM,BOOLEAN,DATEor an integer type up toBIGINT. - Values compare as text. The
bycolumn needs a generator that keeps source labels:categorical,uniformCategory,shuffleLabels,weighted,booleanorconstant. Other generators are refused, also when a default falls back to one. The report warns when most rows find no group. minGroupSizecounts rows, not people. Do not group by a column that holds one value per person, such as a customer id.- A group needs
minGroupSizerows. Sluice uses the 100 largest groups. Other rows use the profile of the whole column. NULLis a group value of its own.- The spike check runs per group. The report counts groups with a spike, but names none.
Expressions¶
expression computes a column with a DuckDB SQL expression over other columns of the same row, for example quantity * unit_price. DuckDB computes it when Sluice writes the snapshot, in the snapshot types.
- Allowed: constants, columns, operators,
CASE,CASTand scalar functions that give the same result each time. - Refused: subqueries, table names, aggregates, window functions, table functions, settings, and functions such as
random()ornow(). - An expression cannot read another
expressioncolumn.template,dateOffset,byandrejectExactMatchescannot name one either. sqlholds at most 1024 bytes, without;, comments or line breaks.sluice config validatechecks only this. The full check runs at build start, before Sluice reads the source.
A template is NULL when a column it reads is NULL. A dateOffset is NULL when from is NULL. With offset: source, the gap keeps its sign: when no source gap is negative, no synthetic gap is either.
Profile on a sample¶
By default a build reads the full source table, once for each profile query. For a large table, set profile.sampleRows on the table, such as profile: { sampleRows: 1000000 }.
- When the table has more rows than
sampleRows, Sluice reads it once and keeps a sample of aboutsampleRowsrows in the builder. Every profile query and the source side of the report then read the sample. - Sluice keeps each row by a hash of its values, its copy number among equal rows and the seed. So the sample size varies a little around
sampleRows. - A table with at most
sampleRowsrows is read in full. Withoutnoisethe build is the same as without the setting. sampleRowsis 1000 to 100 000 000.- The sample follows from the seed and the row values, not from the row order. The same seed and data give the same sample, also from Postgres or MySQL. The DuckDB version must be the same.
- These steps still read the full table: the row count for
sizeandsynthetic.maxRatio,rejectExactMatches, and the source fingerprint ofnoise. minGroupSizecounts the rows in the sample, so rare values drop out sooner. Sluice does not scale the counts up. A scaled count would let a value of one person pass.foreignKeywithfanOut: sourceororphanRate: sourcefails on a sampled table, because each fan-out count would shrink. SetfanOut: { min, max }and a fixedorphanRateinstead.- With
noisethe full row count is one more noisy measurement, also when Sluice does not sample. The noisy count decides whether Sluice samples. - The sample lives in the memory of the builder. A million rows of 20 columns take a few hundred MB. To number equal rows, DuckDB groups the full table once, and it may spill to disk for that.
- The report shows
tables[].profiled_rowsand a warning for each sampled table.
Build and publish¶
Three things start a build:
sluice synth build <name>reads the server config and the policy directory. It needs no running server. See the CLI reference.POST /admin/synthetic/<name>/buildon the admin API builds in the running server.build.onStart: ifMissingbuilds at server start when no snapshot exists. The server serves queries during the build.
The size check runs before the profile. A ratio above synthetic.maxRatio of the server config fails the build, and so does a rows count above that share.
A build writes into synthetic.dir:
| Path | Content |
|---|---|
<name>/snapshots/<build-id>.duckdb | The generated rows. |
<name>/reports/<build-id>.json | The report. Mode 0600. |
<name>/current | The build id that Sluice serves. |
Sluice changes current only after every check passed. A failed build writes its report and leaves current as it was. Sluice keeps the current and the previous snapshot and deletes older ones. Clients can read only the snapshots folder.
The same spec, seed and source data give the same rows on the same CPU architecture. With noise, the DuckDB version must be the same too. The report holds a content_hash of the rows to compare builds.
Status¶
GET /admin/datasources shows a status for each synthetic data source:
| Status | Meaning |
|---|---|
not_built | No snapshot exists. The catalog has no tables. |
ready | The snapshot was built from the current spec. |
stale | The snapshot was built from another spec. Sluice still serves it, logs a warning and sets sluice_synthetic_stale to 1. |
curl -s http://localhost:9091/admin/datasources \
-H "Authorization: Bearer demo-admin-token-change-me"
{
"datasources": [
{
"name": "shop",
"type": "sqlite",
"healthy": true,
"last_check": "<time>",
"schema_pulled_at": "<time>"
},
{
"name": "shopnoisy",
"type": "synthetic",
"healthy": true,
"last_check": "<time>",
"schema_pulled_at": "<time>",
"status": "ready"
},
{
"name": "shopsynth",
"type": "synthetic",
"healthy": true,
"last_check": "<time>",
"schema_pulled_at": "<time>",
"status": "ready"
}
]
}
Report¶
sluice synth report prints the report of the current build. --json prints the whole file, and GET /admin/synthetic/<name> returns it too.
build <build-id> succeeded, origin cli, finished <time>
TABLE COLUMN GENERATOR SOURCE MEDIAN SYNTHETIC MEDIAN DISTANCE WARNINGS
main.customers id sequence 200.5 40.5 - 0
main.customers first_name fake - - - 0
main.customers last_name fake - - - 0
main.customers city categorical - - tv 0.0625 0
main.customers email template - - - 0
main.orders id sequence 1000.5 200.5 - 0
main.orders customer_id foreignKey 189.5 39.5 tv 0.198 0
main.orders region categorical - - tv 0.02 0
main.orders product categorical - - tv 0.05 0
main.orders amount correlated 67.375 67.905 ks 0.0636 0
main.orders quantity correlated 5.5 7 ks 0.12 0
main.orders placed_on dateDistribution 20602.5 20601 ks 0.0567 0
main.orders shipped_on dateOffset 20607.5 20606 ks 0.11 0
correlated main.orders: amount, quantity max_diff 0.028 shrinkage 0.00
DISTANCEcompares source and synthetic data.ksis the largest gap between the two distributions.tvis the share of rows that would need another value.- For
foreignKey,tvcompares the fan-outs. FordateOffset,kscompares the gaps tofrom. - Medians of dates are in days since 1970-01-01. Medians of timestamps are in seconds.
- A spike in a
bygroup gives one warning line with the number of such groups. sluice synth reportprints anoiseline with the budget, and acorrelatedline for each group.- The source
minandmaxin the JSON are not the real extremes. They hold theminGroupSize-th value from each end, or theclipOutliersbounds. - The report holds no source rows, but it holds counts and statistics of the source. Keep it private.
The JSON has these fields for relations, correlated columns and noise. Each one is left out when it does not apply.
| Field | Meaning |
|---|---|
tables[].profiled_rows | The size of the profile sample. See Profile on a sample. |
tables[].depends_on | The parent tables of the foreignKey columns. |
tables[].exact_matches | columns, matched_rows in the first round, redrawn_rows in all rounds, and rounds. |
columns[].depends_on | The columns that this column reads. |
columns[].by | columns, groups, dropped_groups, fallback_rows that used the profile of the whole column, and spike_groups. |
columns[].fan_out | The parent column, the number of parents, the scale and the orphan_rows. |
columns[].words | The number of kept words of sourceVocabulary or sentenceShape. |
noise | epsilon, the number of measurements and epsilon_per_measurement. |
tables[].correlations | Per group: columns, cells per column, shrinkage, the source and synthetic rank correlations, and max_diff. |
columns[].noise_measurements | The noisy measurements of the column. |
columns[].range | With noise: bounds or profile, the origin of the min and max. |
sluice synth build also prints one line for each rejectExactMatches table and each foreignKey column.
Limits¶
- Without
noisethe settings give no formal privacy guarantee. Withnoise,epsiloncovers the counts listed in Noise, not which labels appear and not ranges withoutbounds. rejectExactMatchesstops exact copies, not near copies.sourceVocabularykeeps a name that many rows hold.byuses at most 100 groups.sluice synth buildwrites no record to the audit log. It prints the record on stderr and stores it in the report.- A running server picks up a snapshot from
sluice synth buildonly after a restart. Each build keeps only the current and the previous snapshot, so two CLI builds delete the file a running server serves. Use the admin API to build in the server. - Sluice attaches data sources only at start. Restart it after you add a synthetic data source or change its spec. The seed value is not part of the
stalecheck, so build again after you change it.