[{"data":1,"prerenderedAt":1612},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fgovernance\u002Fdata-contracts":3,"docs-nav":1394},{"id":4,"title":5,"body":6,"description":16,"extension":1386,"meta":1387,"navigation":1389,"path":1390,"seo":1391,"stem":1392,"__hash__":1393},"docs\u002Fdocs\u002Fgovernance\u002Fdata-contracts.md","Data contracts",{"type":7,"value":8,"toc":1372},"minimark",[9,13,17,22,119,123,128,281,290,297,301,383,386,412,435,439,448,494,501,517,520,533,544,548,563,572,650,655,661,834,844,852,865,877,881,899,1226,1229,1267,1277,1281,1328,1332,1368],[10,11,5],"h1",{"id":12},"data-contracts",[14,15,16],"p",{},"A data contract is what a datasource promises to whoever uses it: which columns it has,\nof which type, and which rules its values follow. Tabularia checks the rules every time\nnew data arrives, before it is published: a broken blocking rule refuses the new data,\nand the datasource keeps serving the last good data. It is for whoever owns a\ndatasource that reports, flows and other teams depend on.",[18,19,21],"h2",{"id":20},"at-a-glance","At a glance",[23,24,25,36],"table",{},[26,27,28],"thead",{},[29,30,31,34],"tr",{},[32,33],"th",{},[32,35],{},[37,38,39,56,64,72,87,95,103,111],"tbody",{},[29,40,41,45],{},[42,43,44],"td",{},"Where",[42,46,47,51,52,55],{},[48,49,50],"strong",{},"Datasources",", or Explore: the ",[48,53,54],{},"Data contract"," button on a datasource's row",[29,57,58,61],{},[42,59,60],{},"On what",[42,62,63],{},"any datasource: imported from a database or a file source, or written by a flow or a SQL script",[29,65,66,69],{},[42,67,68],{},"Rules",[42,70,71],{},"column and type, not null, unique, accepted values, range, pattern, row count, freshness, SQL condition; up to 200",[29,73,74,77],{},[42,75,76],{},"Severity",[42,78,79,82,83,86],{},[48,80,81],{},"Warning"," (reported, data published) or ",[48,84,85],{},"Error"," (blocking: data not published)",[29,88,89,92],{},[42,90,91],{},"Checked",[42,93,94],{},"at every refresh, at every flow run or SQL script write into the datasource, when the contract is saved, on demand, and as time passes for freshness",[29,96,97,100],{},[42,98,99],{},"Notifications",[42,101,102],{},"an email when the contract's state changes, through an SMTP connection",[29,104,105,108],{},[42,106,107],{},"Open format",[42,109,110],{},"export as ODCS v3.0.2 (YAML)",[29,112,113,116],{},[42,114,115],{},"Permissions",[42,117,118],{},"read with VIEW on the datasource's folder; write, check and remove with EDIT",[18,120,122],{"id":121},"how-it-works","How it works",[124,125,127],"h3",{"id":126},"the-rules","The rules",[23,129,130,143],{},[26,131,132],{},[29,133,134,137,140],{},[32,135,136],{},"Rule",[32,138,139],{},"In the interface",[32,141,142],{},"Holds when",[37,144,145,161,176,191,206,221,236,251,266],{},[29,146,147,153,158],{},[42,148,149],{},[150,151,152],"code",{},"column",[42,154,155],{},[48,156,157],{},"Column and type",[42,159,160],{},"the column exists and, if a type is chosen, is of that type: integer, number, text, boolean, date, date and time. An integer column satisfies \"number\"",[29,162,163,168,173],{},[42,164,165],{},[150,166,167],{},"not_null",[42,169,170],{},[48,171,172],{},"Not null",[42,174,175],{},"no row has a null in the column",[29,177,178,183,188],{},[42,179,180],{},[150,181,182],{},"unique",[42,184,185],{},[48,186,187],{},"Unique",[42,189,190],{},"no two rows share the same value of one column, or of several together (nulls count as values)",[29,192,193,198,203],{},[42,194,195],{},[150,196,197],{},"accepted_values",[42,199,200],{},[48,201,202],{},"Accepted values",[42,204,205],{},"every non-null value is one of a list (up to 500 values)",[29,207,208,213,218],{},[42,209,210],{},[150,211,212],{},"range",[42,214,215],{},[48,216,217],{},"Range",[42,219,220],{},"every non-null value lies between a minimum and a maximum; either may be left empty",[29,222,223,228,233],{},[42,224,225],{},[150,226,227],{},"pattern",[42,229,230],{},[48,231,232],{},"Pattern",[42,234,235],{},"every non-null value matches a regular expression in full",[29,237,238,243,248],{},[42,239,240],{},[150,241,242],{},"row_count",[42,244,245],{},[48,246,247],{},"Row count",[42,249,250],{},"the number of rows lies between a minimum and a maximum",[29,252,253,258,263],{},[42,254,255],{},[150,256,257],{},"freshness",[42,259,260],{},[48,261,262],{},"Freshness",[42,264,265],{},"the data is at most so many hours old: since the last update, or since the newest value of a date column",[29,267,268,273,278],{},[42,269,270],{},[150,271,272],{},"expression",[42,274,275],{},[48,276,277],{},"SQL condition",[42,279,280],{},"a condition, in Polars SQL, is true on every row; a null result counts as a violation",[14,282,283,284,286,287,289],{},"Every rule has a severity. ",[48,285,81],{}," is not blocking: the new data is published and\nthe violation is reported. ",[48,288,85],{}," is blocking: the new data is not published and the\ndatasource keeps the last good data.",[14,291,292,293,296],{},"A rule that cannot be evaluated (the column is gone, the type does not fit, the SQL does\nnot compile) does ",[48,294,295],{},"not"," hold, and says why: a contract never passes because it could\nnot be checked.",[124,298,300],{"id":299},"when-it-is-checked-and-what-happens","When it is checked, and what happens",[23,302,303,316],{},[26,304,305],{},[29,306,307,310,313],{},[32,308,309],{},"Moment",[32,311,312],{},"Trigger in the history",[32,314,315],{},"Can block",[37,317,318,331,343,356,371],{},[29,319,320,323,328],{},[42,321,322],{},"A refresh of a database datasource, by hand or scheduled",[42,324,325],{},[48,326,327],{},"Refresh",[42,329,330],{},"yes",[29,332,333,336,341],{},[42,334,335],{},"A flow run that publishes into the datasource, a SQL script that writes it",[42,337,338],{},[48,339,340],{},"Flow run",[42,342,330],{},[29,344,345,348,353],{},[42,346,347],{},"The contract is saved",[42,349,350],{},[48,351,352],{},"Contract saved",[42,354,355],{},"no: it says whether the data already there respects it",[29,357,358,363,368],{},[42,359,360],{},[48,361,362],{},"Check now",[42,364,365],{},[48,366,367],{},"Checked by hand",[42,369,370],{},"no",[29,372,373,376,381],{},[42,374,375],{},"Time passing, for a freshness rule on the last update (checked every minute)",[42,377,378],{},[48,379,380],{},"Freshness over time",[42,382,370],{},[14,384,385],{},"On new data, the worker that wrote it evaluates the contract on the result, whatever\nengine produced it, before the gateway swaps the datasource to it:",[387,388,389,396,406],"ul",{},[390,391,392,395],"li",{},[48,393,394],{},"every rule holds, or only warnings fail",": the data is published and the report\nbecomes the contract's state;",[390,397,398,401,402,405],{},[48,399,400],{},"a blocking rule fails",": the run ends as failed with the rules that did not hold,\nthe datasource stays exactly as it was (same rows, same file), the refused file is\ndeleted, and the contract shows ",[48,403,404],{},"Update refused"," until an update succeeds;",[390,407,408,411],{},[48,409,410],{},"the evaluation itself fails"," (the storage does not answer): with even one blocking\nrule the data is not published; with warnings only it is, and the report says why it\ncould not check.",[14,413,414,415,418,419,422,423,422,425,422,428,430,431,434],{},"The state shows next to the datasource wherever it is listed: ",[48,416,417],{},"Not checked",",\n",[48,420,421],{},"Respected",", ",[48,424,81],{},[48,426,427],{},"Violated",[48,429,404],{},". Opening the contract shows\neach rule's outcome, how many rows violate it and up to five sample values, and\n",[48,432,433],{},"History of the checks"," lists every evaluation with its trigger.",[124,436,438],{"id":437},"writing-one","Writing one",[14,440,441,443,444,447],{},[48,442,54],{}," on the datasource's row opens the editor, with the rules grouped by\ncolumn and a ",[48,445,446],{},"Whole table"," group for row count, freshness and SQL conditions.",[387,449,450,462,472,478,488],{},[390,451,452,455,456,458,459,461],{},[48,453,454],{},"Propose from the data"," writes a starting contract from the current data. It is\ncareful on purpose: what is structural (every column and its type, at least one row)\nis proposed as ",[48,457,85],{},"; what is only true today (no nulls, no duplicates, a short\nlist of values) as ",[48,460,81],{},", so the next refresh is not stopped by a case never\nseen. Review it before saving.",[390,463,464,467,468,471],{},[48,465,466],{},"Add rule"," adds one; each rule has its own ",[48,469,470],{},"Severity of the rule",".",[390,473,474,477],{},[48,475,476],{},"Contract active",", unticked, keeps the contract without checking it.",[390,479,480,483,484,487],{},[48,481,482],{},"Save and check"," (or ",[48,485,486],{},"Create the contract"," the first time) saves it and checks it\non the current data. Every save that changes the rules is a new version.",[390,489,490,493],{},[48,491,492],{},"Remove the contract"," stops the checks; the history of the checks stays.",[14,495,496],{},[497,498],"img",{"alt":499,"src":500},"The data contract editor of erp_orders: Respected, the severity legend, Propose from the data, Contract active, and the rules of the columns id, numero, data_ordine and cliente_id with their severity","\u002Fmedia\u002Fguide\u002Fgovernance\u002Fdata-contract-dialog.png",[14,502,503],{},[504,505,506,507,510,511,513,514,516],"em",{},"A contract proposed from the data of an ",[150,508,509],{},"erp_orders"," datasource: column and type as ",[48,512,85],{},", not null and unique as ",[48,515,81],{},", every rule respected at the last check.",[124,518,99],{"id":519},"notifications",[14,521,522,523,422,525,528,529,532],{},"Under ",[48,524,99],{},[48,526,527],{},"Addresses to notify"," and ",[48,530,531],{},"SMTP connection to send with","\nsend an email when the contract's state changes because of new data or of time passing:\nan update refused, a rule no longer respected, everything fine again. One message per\nchange, not one per check: a refresh refused every night sends one email. The first\nsuccessful check of a new contract is not news and sends nothing. Each recipient who\nis a Tabularia user reads it in the language they chose; other addresses in English.",[14,534,535,536,539,540,543],{},"Choosing the connection, or changing the addresses, needs CONNECT on the connection's\nfolder, and the addresses must be within the connection's allowed domains. The scheduler\nsends the notices, and each is recorded in the audit log (",[150,537,538],{},"email.send",", with the reason\n",[150,541,542],{},"contract_state",").",[18,545,547],{"id":546},"example-the-orders-of-the-erp","Example: the orders of the ERP",[14,549,550,551,554,555,558,559,562],{},"The sample data's ",[150,552,553],{},"orders"," datasource (in ",[48,556,557],{},"Sample \u002F Private tables",", imported from the\nERP's ",[150,560,561],{},"vendite.ordini",") feeds the margin and cost-to-serve reports. Its owner wants a\nbroken export from the ERP to stop at the door.",[14,564,565,568,569,571],{},[48,566,567],{},"1. Propose, then tighten."," ",[48,570,454],{}," gives every column with its type,\na row count of at least one, and warnings for the columns without nulls. The owner keeps\nthose, and adds:",[387,573,574,585,594,607,619,629,641],{},[390,575,576,578,579,422,582,584],{},[48,577,187],{}," on ",[150,580,581],{},"id",[48,583,85],{},";",[390,586,587,578,589,422,592,584],{},[48,588,172],{},[150,590,591],{},"cliente_id",[48,593,85],{},[390,595,596,578,598,601,602,418,605,584],{},[48,597,202],{},[150,599,600],{},"canale",": ",[150,603,604],{},"GDO, HoReCa, Grossista, Dettaglio, Gastronomia",[48,606,85],{},[390,608,609,578,611,601,614,422,617,584],{},[48,610,232],{},[150,612,613],{},"numero",[150,615,616],{},"ORD-[0-9]+",[48,618,81],{},[390,620,621,623,624,422,627,584],{},[48,622,247],{}," at least ",[150,625,626],{},"1000",[48,628,85],{},[390,630,631,633,634,637,638,640],{},[48,632,262],{},": at most ",[150,635,636],{},"26"," hours since the last update, ",[48,639,81],{}," (the refresh runs\nevery night);",[390,642,643,568,645,422,648,471],{},[48,644,277],{},[150,646,647],{},"cliente_id > 0 AND agente_id > 0",[48,649,81],{},[14,651,652,654],{},[48,653,482],{},": the current data respects it.",[14,656,657,658,660],{},"The same contract over the API (datasource 12, with an EDIT token; ",[150,659,581],{}," is optional and\nassigned when missing):",[662,663,668],"pre",{"className":664,"code":665,"language":666,"meta":667,"style":667},"language-bash shiki shiki-themes github-dark-dimmed github-light","curl -X PUT https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdatasources\u002F12\u002Fcontract \\\n  -H \"Authorization: Bearer tab_…\" -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"enabled\": true,\n    \"notify_emails\": \"data-team@example.com\",\n    \"notify_connection_id\": 3,\n    \"document\": {\n      \"description\": \"Order headers for the sales reports: one row per order.\",\n      \"rules\": [\n        {\"kind\": \"column\", \"column\": \"id\", \"dtype\": \"integer\", \"severity\": \"error\"},\n        {\"kind\": \"unique\", \"columns\": [\"id\"], \"severity\": \"error\"},\n        {\"kind\": \"not_null\", \"column\": \"cliente_id\", \"severity\": \"error\"},\n        {\"kind\": \"accepted_values\", \"column\": \"canale\",\n         \"values\": [\"GDO\", \"HoReCa\", \"Grossista\", \"Dettaglio\", \"Gastronomia\"], \"severity\": \"error\"},\n        {\"kind\": \"pattern\", \"column\": \"numero\", \"regex\": \"ORD-[0-9]+\", \"severity\": \"warning\"},\n        {\"kind\": \"row_count\", \"min\": 1000, \"severity\": \"error\"},\n        {\"kind\": \"freshness\", \"max_age_hours\": 26, \"severity\": \"warning\"},\n        {\"kind\": \"expression\", \"name\": \"every order has a customer and an agent\",\n         \"sql\": \"cliente_id > 0 AND agente_id > 0\", \"severity\": \"warning\"}\n      ]\n    }\n  }'\n","bash","",[150,669,670,694,711,720,726,732,738,744,750,756,762,768,774,780,786,792,798,804,810,816,822,828],{"__ignoreMap":667},[671,672,675,679,683,687,690],"span",{"class":673,"line":674},"line",1,[671,676,678],{"class":677},"sqRhv","curl",[671,680,682],{"class":681},"swcJU"," -X",[671,684,686],{"class":685},"sXfbr"," PUT",[671,688,689],{"class":685}," https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdatasources\u002F12\u002Fcontract",[671,691,693],{"class":692},"sxsTv"," \\\n",[671,695,697,700,703,706,709],{"class":673,"line":696},2,[671,698,699],{"class":681},"  -H",[671,701,702],{"class":685}," \"Authorization: Bearer tab_…\"",[671,704,705],{"class":681}," -H",[671,707,708],{"class":685}," \"Content-Type: application\u002Fjson\"",[671,710,693],{"class":692},[671,712,714,717],{"class":673,"line":713},3,[671,715,716],{"class":681},"  -d",[671,718,719],{"class":685}," '{\n",[671,721,723],{"class":673,"line":722},4,[671,724,725],{"class":685},"    \"enabled\": true,\n",[671,727,729],{"class":673,"line":728},5,[671,730,731],{"class":685},"    \"notify_emails\": \"data-team@example.com\",\n",[671,733,735],{"class":673,"line":734},6,[671,736,737],{"class":685},"    \"notify_connection_id\": 3,\n",[671,739,741],{"class":673,"line":740},7,[671,742,743],{"class":685},"    \"document\": {\n",[671,745,747],{"class":673,"line":746},8,[671,748,749],{"class":685},"      \"description\": \"Order headers for the sales reports: one row per order.\",\n",[671,751,753],{"class":673,"line":752},9,[671,754,755],{"class":685},"      \"rules\": [\n",[671,757,759],{"class":673,"line":758},10,[671,760,761],{"class":685},"        {\"kind\": \"column\", \"column\": \"id\", \"dtype\": \"integer\", \"severity\": \"error\"},\n",[671,763,765],{"class":673,"line":764},11,[671,766,767],{"class":685},"        {\"kind\": \"unique\", \"columns\": [\"id\"], \"severity\": \"error\"},\n",[671,769,771],{"class":673,"line":770},12,[671,772,773],{"class":685},"        {\"kind\": \"not_null\", \"column\": \"cliente_id\", \"severity\": \"error\"},\n",[671,775,777],{"class":673,"line":776},13,[671,778,779],{"class":685},"        {\"kind\": \"accepted_values\", \"column\": \"canale\",\n",[671,781,783],{"class":673,"line":782},14,[671,784,785],{"class":685},"         \"values\": [\"GDO\", \"HoReCa\", \"Grossista\", \"Dettaglio\", \"Gastronomia\"], \"severity\": \"error\"},\n",[671,787,789],{"class":673,"line":788},15,[671,790,791],{"class":685},"        {\"kind\": \"pattern\", \"column\": \"numero\", \"regex\": \"ORD-[0-9]+\", \"severity\": \"warning\"},\n",[671,793,795],{"class":673,"line":794},16,[671,796,797],{"class":685},"        {\"kind\": \"row_count\", \"min\": 1000, \"severity\": \"error\"},\n",[671,799,801],{"class":673,"line":800},17,[671,802,803],{"class":685},"        {\"kind\": \"freshness\", \"max_age_hours\": 26, \"severity\": \"warning\"},\n",[671,805,807],{"class":673,"line":806},18,[671,808,809],{"class":685},"        {\"kind\": \"expression\", \"name\": \"every order has a customer and an agent\",\n",[671,811,813],{"class":673,"line":812},19,[671,814,815],{"class":685},"         \"sql\": \"cliente_id > 0 AND agente_id > 0\", \"severity\": \"warning\"}\n",[671,817,819],{"class":673,"line":818},20,[671,820,821],{"class":685},"      ]\n",[671,823,825],{"class":673,"line":824},21,[671,826,827],{"class":685},"    }\n",[671,829,831],{"class":673,"line":830},22,[671,832,833],{"class":685},"  }'\n",[14,835,836,839,840,843],{},[48,837,838],{},"2. A bad night."," The ERP starts writing a new channel, ",[150,841,842],{},"Online",", on three orders. The\nnightly refresh reads them, the worker evaluates the contract, and the run fails with:",[662,845,850],{"className":846,"code":848,"language":849},[847],"language-text","Data contract violated: accepted_values on canale (3 rows). The new data was NOT\npublished: the datasource still serves the previous snapshot.\n","text",[150,851,848],{"__ignoreMap":667},[14,853,854,855,857,858,860,861,864],{},"The reports keep reading yesterday's orders. The datasource shows ",[48,856,404],{},", the\nhistory shows the refused check with the sample value ",[150,859,842],{},", and\n",[150,862,863],{},"data-team@example.com"," receives one email.",[14,866,867,870,871,873,874,876],{},[48,868,869],{},"3. The fix."," The owner adds ",[150,872,842],{}," to the accepted values, or the ERP fixes its\nexport; the next refresh passes, the data is published, the state goes back to\n",[48,875,421],{},", and a second email says so.",[18,878,880],{"id":879},"exporting-as-odcs","Exporting as ODCS",[14,882,883,886,887,890,891,894,895,898],{},[48,884,885],{},"Export as ODCS"," downloads the saved contract in the Open Data Contract Standard,\nv3.0.2, as YAML (",[150,888,889],{},"orders.odcs.yaml","), to hand it to a data catalog or another tool. It\nis an export only: the contract in Tabularia stays the source, and there is no import.\nEach column becomes a schema property with its logical and physical type and its\ndescription; each rule becomes a ",[150,892,893],{},"quality"," item with its severity, carrying the\nTabularia rule's id and kind; freshness becomes ",[150,896,897],{},"slaProperties",". An excerpt of the\nexample above:",[662,900,904],{"className":901,"code":902,"language":903,"meta":667,"style":667},"language-yaml shiki shiki-themes github-dark-dimmed github-light","apiVersion: \"v3.0.2\"\nkind: \"DataContract\"\nid: \"urn:tabularia:datasource:12\"\nname: \"orders\"\nversion: \"3.0.0\"\nstatus: \"active\"\nschema:\n  - name: \"orders\"\n    properties:\n      - name: \"canale\"\n        logicalType: \"string\"\n        physicalType: \"String\"\n        quality:\n          - type: \"sql\"\n            name: \"canale has only accepted values\"\n            query: \"SELECT COUNT(*) FROM ${table} WHERE \\\"canale\\\" IS NOT NULL AND \\\"canale\\\" NOT IN ('GDO', 'HoReCa', 'Grossista', 'Dettaglio', 'Gastronomia')\"\n            mustBe: 0\n            unit: \"rows\"\n            dimension: \"conformity\"\n            severity: \"error\"\n    quality:\n      - type: \"library\"\n        rule: \"rowCount\"\n        mustBeGreaterOrEqualTo: 1000\n        unit: \"rows\"\n        severity: \"error\"\nslaProperties:\n  - property: \"latency\"\n    value: 26\n    unit: \"h\"\n","yaml",[150,905,906,918,928,937,947,957,967,975,986,993,1005,1015,1025,1032,1045,1055,1084,1094,1104,1114,1124,1131,1142,1153,1164,1174,1184,1191,1204,1215],{"__ignoreMap":667},[671,907,908,912,915],{"class":673,"line":674},[671,909,911],{"class":910},"shb1k","apiVersion",[671,913,601],{"class":914},"ssh_m",[671,916,917],{"class":685},"\"v3.0.2\"\n",[671,919,920,923,925],{"class":673,"line":696},[671,921,922],{"class":910},"kind",[671,924,601],{"class":914},[671,926,927],{"class":685},"\"DataContract\"\n",[671,929,930,932,934],{"class":673,"line":713},[671,931,581],{"class":910},[671,933,601],{"class":914},[671,935,936],{"class":685},"\"urn:tabularia:datasource:12\"\n",[671,938,939,942,944],{"class":673,"line":722},[671,940,941],{"class":910},"name",[671,943,601],{"class":914},[671,945,946],{"class":685},"\"orders\"\n",[671,948,949,952,954],{"class":673,"line":728},[671,950,951],{"class":910},"version",[671,953,601],{"class":914},[671,955,956],{"class":685},"\"3.0.0\"\n",[671,958,959,962,964],{"class":673,"line":734},[671,960,961],{"class":910},"status",[671,963,601],{"class":914},[671,965,966],{"class":685},"\"active\"\n",[671,968,969,972],{"class":673,"line":740},[671,970,971],{"class":910},"schema",[671,973,974],{"class":914},":\n",[671,976,977,980,982,984],{"class":673,"line":746},[671,978,979],{"class":914},"  - ",[671,981,941],{"class":910},[671,983,601],{"class":914},[671,985,946],{"class":685},[671,987,988,991],{"class":673,"line":752},[671,989,990],{"class":910},"    properties",[671,992,974],{"class":914},[671,994,995,998,1000,1002],{"class":673,"line":758},[671,996,997],{"class":914},"      - ",[671,999,941],{"class":910},[671,1001,601],{"class":914},[671,1003,1004],{"class":685},"\"canale\"\n",[671,1006,1007,1010,1012],{"class":673,"line":764},[671,1008,1009],{"class":910},"        logicalType",[671,1011,601],{"class":914},[671,1013,1014],{"class":685},"\"string\"\n",[671,1016,1017,1020,1022],{"class":673,"line":770},[671,1018,1019],{"class":910},"        physicalType",[671,1021,601],{"class":914},[671,1023,1024],{"class":685},"\"String\"\n",[671,1026,1027,1030],{"class":673,"line":776},[671,1028,1029],{"class":910},"        quality",[671,1031,974],{"class":914},[671,1033,1034,1037,1040,1042],{"class":673,"line":782},[671,1035,1036],{"class":914},"          - ",[671,1038,1039],{"class":910},"type",[671,1041,601],{"class":914},[671,1043,1044],{"class":685},"\"sql\"\n",[671,1046,1047,1050,1052],{"class":673,"line":788},[671,1048,1049],{"class":910},"            name",[671,1051,601],{"class":914},[671,1053,1054],{"class":685},"\"canale has only accepted values\"\n",[671,1056,1057,1060,1062,1065,1068,1070,1072,1075,1077,1079,1081],{"class":673,"line":794},[671,1058,1059],{"class":910},"            query",[671,1061,601],{"class":914},[671,1063,1064],{"class":685},"\"SELECT COUNT(*) FROM ${table} WHERE ",[671,1066,1067],{"class":692},"\\\"",[671,1069,600],{"class":685},[671,1071,1067],{"class":692},[671,1073,1074],{"class":685}," IS NOT NULL AND ",[671,1076,1067],{"class":692},[671,1078,600],{"class":685},[671,1080,1067],{"class":692},[671,1082,1083],{"class":685}," NOT IN ('GDO', 'HoReCa', 'Grossista', 'Dettaglio', 'Gastronomia')\"\n",[671,1085,1086,1089,1091],{"class":673,"line":800},[671,1087,1088],{"class":910},"            mustBe",[671,1090,601],{"class":914},[671,1092,1093],{"class":681},"0\n",[671,1095,1096,1099,1101],{"class":673,"line":806},[671,1097,1098],{"class":910},"            unit",[671,1100,601],{"class":914},[671,1102,1103],{"class":685},"\"rows\"\n",[671,1105,1106,1109,1111],{"class":673,"line":812},[671,1107,1108],{"class":910},"            dimension",[671,1110,601],{"class":914},[671,1112,1113],{"class":685},"\"conformity\"\n",[671,1115,1116,1119,1121],{"class":673,"line":818},[671,1117,1118],{"class":910},"            severity",[671,1120,601],{"class":914},[671,1122,1123],{"class":685},"\"error\"\n",[671,1125,1126,1129],{"class":673,"line":824},[671,1127,1128],{"class":910},"    quality",[671,1130,974],{"class":914},[671,1132,1133,1135,1137,1139],{"class":673,"line":830},[671,1134,997],{"class":914},[671,1136,1039],{"class":910},[671,1138,601],{"class":914},[671,1140,1141],{"class":685},"\"library\"\n",[671,1143,1145,1148,1150],{"class":673,"line":1144},23,[671,1146,1147],{"class":910},"        rule",[671,1149,601],{"class":914},[671,1151,1152],{"class":685},"\"rowCount\"\n",[671,1154,1156,1159,1161],{"class":673,"line":1155},24,[671,1157,1158],{"class":910},"        mustBeGreaterOrEqualTo",[671,1160,601],{"class":914},[671,1162,1163],{"class":681},"1000\n",[671,1165,1167,1170,1172],{"class":673,"line":1166},25,[671,1168,1169],{"class":910},"        unit",[671,1171,601],{"class":914},[671,1173,1103],{"class":685},[671,1175,1177,1180,1182],{"class":673,"line":1176},26,[671,1178,1179],{"class":910},"        severity",[671,1181,601],{"class":914},[671,1183,1123],{"class":685},[671,1185,1187,1189],{"class":673,"line":1186},27,[671,1188,897],{"class":910},[671,1190,974],{"class":914},[671,1192,1194,1196,1199,1201],{"class":673,"line":1193},28,[671,1195,979],{"class":914},[671,1197,1198],{"class":910},"property",[671,1200,601],{"class":914},[671,1202,1203],{"class":685},"\"latency\"\n",[671,1205,1207,1210,1212],{"class":673,"line":1206},29,[671,1208,1209],{"class":910},"    value",[671,1211,601],{"class":914},[671,1213,1214],{"class":681},"26\n",[671,1216,1218,1221,1223],{"class":673,"line":1217},30,[671,1219,1220],{"class":910},"    unit",[671,1222,601],{"class":914},[671,1224,1225],{"class":685},"\"h\"\n",[18,1227,115],{"id":1228},"permissions",[23,1230,1231,1241],{},[26,1232,1233],{},[29,1234,1235,1238],{},[32,1236,1237],{},"Action",[32,1239,1240],{},"Needs",[37,1242,1243,1251,1259],{},[29,1244,1245,1248],{},[42,1246,1247],{},"See the state and open the contract, its history, the ODCS export",[42,1249,1250],{},"VIEW on the datasource's folder",[29,1252,1253,1256],{},[42,1254,1255],{},"Save, check, propose, remove",[42,1257,1258],{},"EDIT on the datasource's folder",[29,1260,1261,1264],{},[42,1262,1263],{},"See and change who is notified",[42,1265,1266],{},"EDIT on the datasource's folder; CONNECT on the SMTP connection to choose it or to change the addresses",[14,1268,1269,1270,418,1273,1276],{},"Saving and removing a contract are in the audit log (",[150,1271,1272],{},"contract.save",[150,1274,1275],{},"contract.delete","). A refused update is a failed run, with the broken rules as its\nerror, in the run history.",[18,1278,1280],{"id":1279},"limits-and-good-to-know","Limits and good to know",[387,1282,1283,1289,1295,1307,1316,1322],{},[390,1284,1285,1288],{},[48,1286,1287],{},"One contract per datasource",", up to 200 rules; an accepted-values list holds up to\n500 values, a regular expression up to 500 characters, a SQL condition up to 2,000.",[390,1290,1291,1294],{},[48,1292,1293],{},"Polars dialects."," The regular expressions and SQL conditions are evaluated by\nPolars, whatever engine produced the data. A SQL condition is an expression on one\nrow, not a query: it cannot read another table. Compare dates with date columns, not\nwith text literals.",[390,1296,1297,1300,1301,1303,1304,1306],{},[48,1298,1299],{},"A check by hand waits two minutes."," On a very large datasource, ",[48,1302,362],{}," and\n",[48,1305,482],{}," may give up (\"The dataset is too large to be checked\ninteractively\"): the contract is saved and is checked at the next update.",[390,1308,1309,1312,1313,1315],{},[48,1310,1311],{},"Freshness on a date column"," is evaluated on the data, so its outcome changes only\nwhen the data is checked again (new data, or ",[48,1314,362],{},"); freshness on the last\nupdate changes with the clock.",[390,1317,1318,1321],{},[48,1319,1320],{},"Python and R scripts"," that publish their output as a datasource do not check its\ncontract.",[390,1323,1324,1327],{},[48,1325,1326],{},"A contract removed or turned off during a run"," decides nothing for that run.",[18,1329,1331],{"id":1330},"related","Related",[387,1333,1334,1342,1348,1354,1361],{},[390,1335,1336,1341],{},[1337,1338,1340],"a",{"href":1339},"\u002Fdocs\u002Fgovernance","Security and governance",": the map.",[390,1343,1344,1347],{},[1337,1345,115],{"href":1346},"\u002Fdocs\u002Fgovernance\u002Fpermissions",": VIEW, EDIT, CONNECT.",[390,1349,1350,471],{},[1337,1351,1353],{"href":1352},"\u002Fdocs\u002Fgovernance\u002Faudit-log","Audit log",[390,1355,1356,1360],{},[1337,1357,1359],{"href":1358},"\u002Fdocs\u002Fsql-scripts","SQL scripts",": a script that writes a datasource.",[390,1362,1363,1367],{},[1337,1364,1366],{"href":1365},"\u002Fdocs\u002Fconnections","Connections",": the SMTP connection for the notices.",[1369,1370,1371],"style",{},"html pre.shiki code .sqRhv, html code.shiki .sqRhv{--shiki-default:#F69D50;--shiki-light:#6F42C1}html pre.shiki code .swcJU, html code.shiki .swcJU{--shiki-default:#6CB6FF;--shiki-light:#005CC5}html pre.shiki code .sXfbr, html code.shiki .sXfbr{--shiki-default:#96D0FF;--shiki-light:#032F62}html pre.shiki code .sxsTv, html code.shiki .sxsTv{--shiki-default:#F47067;--shiki-light:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .shb1k, html code.shiki .shb1k{--shiki-default:#8DDB8C;--shiki-light:#22863A}html pre.shiki code .ssh_m, html code.shiki .ssh_m{--shiki-default:#ADBAC7;--shiki-light:#24292E}",{"title":667,"searchDepth":696,"depth":713,"links":1373},[1374,1375,1381,1382,1383,1384,1385],{"id":20,"depth":696,"text":21},{"id":121,"depth":696,"text":122,"children":1376},[1377,1378,1379,1380],{"id":126,"depth":713,"text":127},{"id":299,"depth":713,"text":300},{"id":437,"depth":713,"text":438},{"id":519,"depth":713,"text":99},{"id":546,"depth":696,"text":547},{"id":879,"depth":696,"text":880},{"id":1228,"depth":696,"text":115},{"id":1279,"depth":696,"text":1280},{"id":1330,"depth":696,"text":1331},"md",{"source":1388},"docs\u002Fgovernance\u002Fdata-contracts.md",true,"\u002Fdocs\u002Fgovernance\u002Fdata-contracts",{"title":5,"description":16},"docs\u002Fgovernance\u002Fdata-contracts","x0Qu0lWEtAfR6GOWNcAjlkWxc1alckxQUmd7FeDtYcU",[1395,1414,1468,1500,1530,1559,1578,1604],{"title":1396,"icon":1397,"intro":1398,"items":1399},"Getting started","start","Install it on one machine, learn the few ideas everything else is built on, and run a first flow.",[1400,1402,1405,1408,1411],{"path":1401,"title":1396},"\u002Fdocs\u002Fgetting-started",{"path":1403,"title":1404},"\u002Fdocs\u002Fgetting-started-docker-compose","Walkthrough with Docker Compose",{"path":1406,"title":1407},"\u002Fdocs\u002Fdeploy\u002Fminikube","Try it on your computer (minikube)",{"path":1409,"title":1410},"\u002Fdocs\u002Fconcepts","Concepts",{"path":1412,"title":1413},"\u002Fdocs\u002Foverview","Overview (README)",{"title":1415,"icon":1416,"intro":1417,"items":1418},"Capabilities","capabilities","What you can build: visual flows, analysis on any step, AI questions, pixel-perfect reports, dashboards, Python, R and SQL scripts, containers and the orchestration that ties them together.",[1419,1422,1425,1428,1431,1434,1437,1440,1443,1444,1447,1450,1453,1456,1459,1462,1465],{"path":1420,"title":1421},"\u002Fdocs\u002Fcapabilities","All capabilities",{"path":1423,"title":1424},"\u002Fdocs\u002Fcapabilities\u002Fflows","Low-code flows",{"path":1426,"title":1427},"\u002Fdocs\u002Fcapabilities\u002Fpreview-and-analysis","Preview and analysis",{"path":1429,"title":1430},"\u002Fdocs\u002Fcapabilities\u002Fask-your-data","Ask your data with AI",{"path":1432,"title":1433},"\u002Fdocs\u002Fcapabilities\u002Freports","Pixel-perfect reports",{"path":1435,"title":1436},"\u002Fdocs\u002Fcapabilities\u002Freport-functions","Report functions",{"path":1438,"title":1439},"\u002Fdocs\u002Fcapabilities\u002Fdashboards","Dashboards",{"path":1441,"title":1442},"\u002Fdocs\u002Fscripting","Python and R",{"path":1358,"title":1359},{"path":1445,"title":1446},"\u002Fdocs\u002Fcapabilities\u002Fnotebooks","marimo notebooks as apps",{"path":1448,"title":1449},"\u002Fdocs\u002Fcontainer-node","Docker images in flows",{"path":1451,"title":1452},"\u002Fdocs\u002Fcapabilities\u002Fflow-groups","Flow groups: orchestration",{"path":1454,"title":1455},"\u002Fdocs\u002Fcapabilities\u002Fscheduling","Scheduling",{"path":1457,"title":1458},"\u002Fdocs\u002Fpushmail","Pushmail: datasources, reports and dashboards by email",{"path":1460,"title":1461},"\u002Fdocs\u002Fplaceholders","Dynamic dates",{"path":1463,"title":1464},"\u002Fdocs\u002Fcapabilities\u002Fengines","Engines",{"path":1466,"title":1467},"\u002Fdocs\u002Fengines\u002Fengine-differences","Engine differences",{"title":1469,"icon":1470,"intro":1471,"items":1472},"Sources","sources","Every way data comes in: files, databases, buckets, SFTP folders, HTTP APIs, SharePoint, Google Sheets, and the results of your own flows.",[1473,1476,1479,1482,1485,1488,1491,1494,1497],{"path":1474,"title":1475},"\u002Fdocs\u002Fsources","All sources",{"path":1477,"title":1478},"\u002Fdocs\u002Fsources\u002Ffiles","Uploaded files",{"path":1480,"title":1481},"\u002Fdocs\u002Fsources\u002Fdatabases","Database tables and queries",{"path":1483,"title":1484},"\u002Fdocs\u002Fs3-datasource","S3 buckets",{"path":1486,"title":1487},"\u002Fdocs\u002Fsftp","SFTP folders",{"path":1489,"title":1490},"\u002Fdocs\u002Fapi-datasource","HTTP APIs",{"path":1492,"title":1493},"\u002Fdocs\u002Fsources\u002Fsharepoint","Excel on SharePoint",{"path":1495,"title":1496},"\u002Fdocs\u002Fsources\u002Fgoogle-sheets","Google Sheets",{"path":1498,"title":1499},"\u002Fdocs\u002Fsources\u002Fflow-outputs","Results of flows and scripts",{"title":1366,"icon":1501,"intro":1502,"items":1503},"connections","The systems Tabularia reads from and writes to, how each connection is set up, and who may use it.",[1504,1506,1509,1512,1515,1517,1518,1520,1521,1524,1527],{"path":1365,"title":1505},"All connections",{"path":1507,"title":1508},"\u002Fdocs\u002Fconnections\u002Fdatabases","Databases",{"path":1510,"title":1511},"\u002Fdocs\u002Fsqlserver","Microsoft SQL Server driver",{"path":1513,"title":1514},"\u002Fdocs\u002Fconnections\u002Fs3","S3 and compatible storage",{"path":1486,"title":1516},"SFTP servers",{"path":1489,"title":1490},{"path":1492,"title":1519},"SharePoint",{"path":1495,"title":1496},{"path":1522,"title":1523},"\u002Fdocs\u002Fconnections\u002Fsmtp","SMTP email",{"path":1525,"title":1526},"\u002Fdocs\u002Fcontainer-registries","Container registries",{"path":1528,"title":1529},"\u002Fdocs\u002Fconnections\u002Fdestinations","Writing data out",{"title":1531,"icon":1532,"intro":1533,"items":1534},"Integrations","integrations","How Tabularia fits your stack: AI providers, the REST API and webhooks, dbt, OpenLineage, single sign-on, SIEM and monitoring.",[1535,1538,1541,1544,1547,1550,1553,1556],{"path":1536,"title":1537},"\u002Fdocs\u002Fintegrations","All integrations",{"path":1539,"title":1540},"\u002Fdocs\u002Fintegrations\u002Fai","AI: where it is used",{"path":1542,"title":1543},"\u002Fdocs\u002Fapi","API, tokens and webhooks",{"path":1545,"title":1546},"\u002Fdocs\u002Fdbt-export","Export to dbt",{"path":1548,"title":1549},"\u002Fdocs\u002Fopenlineage","OpenLineage",{"path":1551,"title":1552},"\u002Fdocs\u002Fsso-keycloak","Single sign-on with Keycloak",{"path":1554,"title":1555},"\u002Fdocs\u002Fsiem-splunk","Audit log to Splunk",{"path":1557,"title":1558},"\u002Fdocs\u002Fintegrations\u002Fmonitoring","Monitoring",{"title":1560,"icon":1561,"intro":1562,"items":1563},"Security & governance","governance","Who may see and do what, what is recorded, and how data quality and personal data are kept in check.",[1564,1566,1567,1568,1569,1572,1575],{"path":1339,"title":1565},"Overview",{"path":1346,"title":115},{"path":1352,"title":1353},{"path":1390,"title":5},{"path":1570,"title":1571},"\u002Fdocs\u002Fdesign\u002Fsso-group-mapping","SSO and group mapping",{"path":1573,"title":1574},"\u002Fdocs\u002Fexamples\u002Fkeycloak","Keycloak example on a laptop",{"path":1576,"title":1577},"\u002Fdocs\u002Fengines\u002Fclickhouse-ai-user","A ClickHouse user for the AI assistant",{"title":1579,"icon":1580,"intro":1581,"items":1582},"Install & operate","operate","Run it in production on Kubernetes, configure it, upgrade it and keep it healthy.",[1583,1586,1589,1592,1595,1598,1601],{"path":1584,"title":1585},"\u002Fdocs\u002Fdeploy\u002Fkubernetes","Kubernetes and Helm",{"path":1587,"title":1588},"\u002Fdocs\u002Fdeploy\u002Fkind","A company-like cluster on one computer (kind)",{"path":1590,"title":1591},"\u002Fdocs\u002Fdeploy\u002Fupgrading","Upgrading, backups and restore",{"path":1593,"title":1594},"\u002Fdocs\u002Fdeploy\u002Frelease-checklist","Install and upgrade checklist",{"path":1596,"title":1597},"\u002Fdocs\u002Fconfiguration","Configuration reference",{"path":1599,"title":1600},"\u002Fdocs\u002Fengine-settings","Settings from the panel",{"path":1602,"title":1603},"\u002Fdocs\u002Fdatabase-migrations","Database migrations",{"title":1605,"icon":1606,"intro":1607,"items":1608},"Reference","reference","Release process and the remaining reference pages.",[1609],{"path":1610,"title":1611},"\u002Fdocs\u002Frelease-process","Release process",1793656372191]