Add dedicated documentation on how to model data and relations with SQLite and TrailBase.

This commit is contained in:
Sebastian Jeltsch
2025-02-03 15:06:38 +01:00
parent 31221ee8ab
commit ea61aeab45
3 changed files with 1080 additions and 0 deletions

View File

@@ -0,0 +1,797 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="505.58176"
height="377.57263"
viewBox="0 0 133.7685 99.899433"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="_relations.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="0.78095874"
inkscape:cx="617.83034"
inkscape:cy="257.37595"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g2790" />
<defs
id="defs2">
<marker
style="overflow:visible"
id="marker6535"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="TriangleStart"
markerWidth="5.3244081"
markerHeight="6.155385"
viewBox="0 0 5.3244081 6.1553851"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path6533" />
</marker>
<filter
id="mask-powermask-path-effect2736_inverse"
inkscape:label="filtermask-powermask-path-effect2736"
style="color-interpolation-filters:sRGB"
height="100"
width="100"
x="-50"
y="-50">
<feColorMatrix
id="mask-powermask-path-effect2736_primitive1"
values="1"
type="saturate"
result="fbSourceGraphic" />
<feColorMatrix
id="mask-powermask-path-effect2736_primitive2"
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "
in="fbSourceGraphic" />
</filter>
<filter
id="mask-powermask-path-effect2758_inverse"
inkscape:label="filtermask-powermask-path-effect2758"
style="color-interpolation-filters:sRGB"
height="100"
width="100"
x="-50"
y="-50">
<feColorMatrix
id="mask-powermask-path-effect2758_primitive1"
values="1"
type="saturate"
result="fbSourceGraphic" />
<feColorMatrix
id="mask-powermask-path-effect2758_primitive2"
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "
in="fbSourceGraphic" />
</filter>
<mask
maskUnits="userSpaceOnUse"
id="mask2782">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2784"
width="43.127083"
height="47.095833"
x="45.057217"
y="73.149208"
rx="2.6458333"
ry="2.6458333" />
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask2782-3">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2784-6"
width="43.127083"
height="47.095833"
x="45.057217"
y="73.149208"
rx="2.6458333"
ry="2.6458333" />
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask2782-3-9">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2784-6-3"
width="43.127083"
height="47.095833"
x="45.057217"
y="73.149208"
rx="2.6458333"
ry="2.6458333" />
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask2782-3-9-7">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2784-6-3-5"
width="43.127083"
height="47.095833"
x="45.057217"
y="73.149208"
rx="2.6458333"
ry="2.6458333" />
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask2782-3-9-7-1">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2784-6-3-5-9"
width="43.127083"
height="47.095833"
x="45.057217"
y="73.149208"
rx="2.6458333"
ry="2.6458333" />
</mask>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-64.98357,-9.6775795)">
<g
id="g2790"
transform="translate(1.6420904,-22.808358)">
<g
id="g6421"
transform="translate(-0.5951938,1.8160305e-5)">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2744-7-6"
width="36.97929"
height="47.095833"
x="69.010262"
y="35.987774"
rx="2.2686679"
ry="2.6458333" />
<rect
style="fill:#87de87;stroke:#000000;stroke-width:0.571464;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2714-5-0"
width="51.557812"
height="13.852211"
x="40.185043"
y="70.225372"
rx="2.36678"
ry="2.8985407"
mask="url(#mask2782-3-9)"
transform="matrix(0.85744936,0,0,1,30.375993,-37.161433)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:6.35px;line-height:0em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="87.413101"
y="43.286297"
id="text656-3-6"><tspan
sodipodi:role="line"
id="tspan654-5-2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#000000;stroke:none;stroke-width:1.25651"
x="87.413101"
y="43.286297">post</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:5.29167px;line-height:1.5em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="72.020676"
y="54.841846"
id="text4262-6-6"><tspan
sodipodi:role="line"
id="tspan4260-2-1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="54.841846">• id</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="62.77935"
id="tspan4376-9-8">• author</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="70.716858"
id="tspan6181">• title</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="78.654358"
id="tspan4631-7">• body</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="86.591866"
id="tspan4378-1-9" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="94.529373"
id="tspan4370-2-2" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="102.46687"
id="tspan4366-7-0" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="72.020676"
y="110.40438"
id="tspan4368-0-2" /></text>
</g>
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2744-7-6-9-4"
width="35.21933"
height="32.737587"
x="115.9934"
y="35.987774"
rx="2.2686682"
ry="2.6458333" />
<rect
style="fill:#ffe680;stroke:#000000;stroke-width:0.585569;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2714-5-0-2-7"
width="51.557812"
height="13.852211"
x="40.185043"
y="70.225372"
rx="2.4850514"
ry="2.8985407"
mask="url(#mask2782-3-9-7-1)"
transform="matrix(0.81664066,0,0,1,79.197829,-37.161415)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="133.60304"
y="43.286297"
id="text656-3-6-2-8"><tspan
sodipodi:role="line"
id="tspan654-5-2-8-4"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.7625px;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#000000;stroke:none;stroke-width:1.25651"
x="133.60304"
y="43.286297">post_tag</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:5.29167px;line-height:1.5em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="117.63883"
y="54.841846"
id="text4262-6-6-9-5"><tspan
sodipodi:role="line"
id="tspan4260-2-1-7-0"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="54.841846">• post</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="62.77935"
id="tspan4631-7-6-1">• tag</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="70.716858"
id="tspan4378-1-9-1-0" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="78.654358"
id="tspan4370-2-2-2-6" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="86.591866"
id="tspan4366-7-0-9-3" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="117.63883"
y="94.529373"
id="tspan4368-0-2-3-2" /></text>
<g
id="g720-3"
transform="translate(72.757314,9.7043009)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7"><tspan
sodipodi:role="line"
id="tspan661-5"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">FK</tspan></text>
</g>
<g
id="g3167"
transform="translate(59.546151,4.8493188)">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2744-7"
width="41.781292"
height="40.334202"
x="9.9245825"
y="86.937263"
rx="2.2686679"
ry="2.6458333" />
<rect
style="fill:#87cdde;stroke:#000000;stroke-width:0.537622;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2714-5"
width="51.557812"
height="13.852211"
x="40.185043"
y="70.225372"
rx="2.0947616"
ry="2.8985407"
mask="url(#mask2782-3)"
transform="matrix(0.9687947,0,0,1,-33.726604,13.788048)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:6.35px;line-height:0em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="31.020739"
y="94.235786"
id="text656-3"><tspan
sodipodi:role="line"
id="tspan654-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#000000;stroke:none;stroke-width:1.25651"
x="31.020739"
y="94.235786">profile</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:5.29167px;line-height:1.5em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="11.394978"
y="105.7913"
id="text4262-6"><tspan
sodipodi:role="line"
id="tspan4260-2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="105.7913">• id</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="113.72881"
id="tspan4376-9">• user</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="121.66631"
id="tspan4631">• name</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="129.60381"
id="tspan5873" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="137.54132"
id="tspan4378-1" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="145.47882"
id="tspan4370-2" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="153.41632"
id="tspan4366-7" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="11.394978"
y="161.35384"
id="tspan4368-0" /></text>
<g
id="g720"
transform="translate(6.7702711,60.666195)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663"><tspan
sodipodi:role="line"
id="tspan661"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">FK</tspan></text>
</g>
<g
id="g720-36"
transform="translate(18.56777,60.829586)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-7"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:3.70417px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-5"><tspan
sodipodi:role="line"
id="tspan661-3"
style="font-size:3.70417px;fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">Uniq</tspan></text>
</g>
<g
id="g1291"
transform="translate(0.12494804,52.419428)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-6"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-2"><tspan
sodipodi:role="line"
id="tspan661-5-6"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">PK</tspan></text>
</g>
</g>
<g
id="g1291-9"
transform="translate(61.316649,1.369148)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-6-2"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-2-0"><tspan
sodipodi:role="line"
id="tspan661-5-6-2"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">PK</tspan></text>
</g>
<g
id="g639"
transform="translate(65.284322,-9.6728225)">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2744"
width="36.97929"
height="33.131294"
x="68.767479"
y="101.4594"
rx="2.2686679"
ry="2.6458333" />
<rect
style="fill:#eeaaff;stroke:#000000;stroke-width:0.571464;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2714"
width="51.557812"
height="13.852211"
x="40.185043"
y="70.225372"
rx="2.36678"
ry="2.8985407"
mask="url(#mask2782)"
transform="matrix(0.85744936,0,0,1,30.133193,28.310199)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:6.35px;line-height:0em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="86.767235"
y="108.75793"
id="text656"><tspan
sodipodi:role="line"
id="tspan654"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#000000;stroke:none;stroke-width:1.25651"
x="86.767235"
y="108.75793">_user</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:5.29167px;line-height:1.5em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="71.777885"
y="120.31348"
id="text4262"><tspan
sodipodi:role="line"
id="tspan4260"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="120.31348">• id</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="128.25098"
id="tspan4376">• ...</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="136.18849"
id="tspan4378" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="144.12599"
id="tspan4370" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="152.06349"
id="tspan4366" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="71.777885"
y="160.00101"
id="tspan4368" /></text>
<g
id="g1291-9-3"
transform="translate(60.685222,66.720435)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-6-2-7"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-2-0-5"><tspan
sodipodi:role="line"
id="tspan661-5-6-2-9"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">PK</tspan></text>
</g>
</g>
<g
id="g720-3-3"
transform="translate(113.65745,1.5521743)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-5"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-6"><tspan
sodipodi:role="line"
id="tspan661-5-2"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">FK</tspan></text>
</g>
<g
id="g700"
transform="translate(-6.7304382)">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2744-7-6-9"
width="36.97929"
height="32.98344"
x="166.59656"
y="35.987774"
rx="2.2686679"
ry="2.6458333" />
<rect
style="fill:#ffccaa;stroke:#000000;stroke-width:0.571464;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
id="rect2714-5-0-2"
width="51.557812"
height="13.852211"
x="40.185043"
y="70.225372"
rx="2.36678"
ry="2.8985407"
mask="url(#mask2782-3-9-7)"
transform="matrix(0.85744936,0,0,1,127.96296,-37.161423)" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:6.35px;line-height:0em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="184.99913"
y="43.286297"
id="text656-3-6-2"><tspan
sodipodi:role="line"
id="tspan654-5-2-8"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#000000;stroke:none;stroke-width:1.25651"
x="184.99913"
y="43.286297">tag</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:5.29167px;line-height:1.5em;font-family:Inter;-inkscape-font-specification:'Inter Bold';fill:none;stroke:#000000;stroke-width:1.25651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="169.60693"
y="54.841846"
id="text4262-6-6-9"><tspan
sodipodi:role="line"
id="tspan4260-2-1-7"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="54.841846">• id</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="62.77935"
id="tspan4376-9-8-3">• label</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="70.716858"
id="tspan4631-7-6" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="78.654358"
id="tspan4378-1-9-1" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="86.591866"
id="tspan4370-2-2-2" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="94.529373"
id="tspan4366-7-0-9" /><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;font-family:Inter;-inkscape-font-specification:Inter;fill:#000000;stroke:none;stroke-width:1.25651"
x="169.60693"
y="102.46687"
id="tspan4368-0-2-3" /></text>
<g
id="g1291-9-3-2"
transform="translate(158.40392,1.264378)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-6-2-7-2"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-2-0-5-8"><tspan
sodipodi:role="line"
id="tspan661-5-6-2-9-9"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">PK</tspan></text>
</g>
</g>
<g
id="g720-3-3-9"
transform="translate(113.68575,9.3405403)">
<rect
style="fill:#ececec;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
id="rect659-6-5-1"
width="10.015675"
height="6.3694124"
x="21.495552"
y="48.592373"
rx="1.5875"
ry="1.5875" />
<text
xml:space="preserve"
style="font-size:4.7625px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#ffffff;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40"
x="26.360374"
y="53.309708"
id="text663-7-6-2"><tspan
sodipodi:role="line"
id="tspan661-5-2-7"
style="fill:#000000;stroke:none;stroke-width:0.529167"
x="26.360374"
y="53.309708">FK</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none;marker-end:url(#marker6535)"
d="m 72.865201,61.260787 h -9.254692 v 23.957292 h 68.320101 l 0.078,18.446701 4.46379,3.65987"
id="path5929"
sodipodi:nodetypes="cccccc" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;line-height:0em;font-family:Inter;-inkscape-font-specification:Inter;text-align:center;text-anchor:middle;fill:#87de87;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
x="76.259377"
y="56.282932"
id="text6177"><tspan
sodipodi:role="line"
id="tspan6175"
style="stroke-width:0.529167"
x="76.259377"
y="56.282932" /><tspan
sodipodi:role="line"
style="stroke-width:0.529167"
id="tspan6179"
x="76.259377"
y="56.282932" /></text>
<path
style="fill:none;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none;marker-end:url(#marker6535)"
d="m 118.97093,61.264835 h -7.81308 v 10.932692 h 42.90504 V 53.287849 h 7.8427"
id="path6483"
sodipodi:nodetypes="cccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none"
d="m 118.99767,53.316534 h -8.32273 V 32.750521 H 63.606063 v 20.524647 h 8.521551"
id="path6485"
sodipodi:nodetypes="cccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:40;stroke-dasharray:none;marker-end:url(#marker6535)"
d="M 72.31685,109.0099 H 63.610509 V 88.77312 h 64.736831 v 25.32127 h 3.56458 l 4.40153,-3.43366"
id="path1264"
sodipodi:nodetypes="ccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,238 @@
---
title: Models & Relations
---
import { Image } from "astro:assets";
import { Aside } from "@astrojs/starlight/components";
import foo from "./_relations.svg"
## Modeling Data
TrailBase gives you full, untethered access to SQLite, as such data is modeled
on top of ISO SQL and SQLite concepts.
This means that all data is organized as rows or records within columns of a
table as defined by the table schema.
Relationships between records are expressed by simply referencing other records
via their primary key. Relationships commonly cross table boundaries.
Data can then be *joined* together within the same database at query time.
If you're new to SQL and this sounds abstract, don't worry it will become clear
very soon.
One of the main benefits of SQL databases is that you can define your models
based on intrinsic properties of the underlying data and their relations,
rather than having to worry about how the data is or will be used in the future.
Instead, SQL *queries* let you flexibly define what data is being accessed, how
it's transformed and what's returned.
*How* data is accessed is never explicitly defined and is derived by the
[*query optimizer*](https://sqlite.org/optoverview.html).
This means, if you discover new use-cases in your data that require combining
data in ways that would be slow, you can optimize it after the fact by adding
ore removing indexes typically without ever having to touch the models, the
queries, or the downstream code consuming the data 🎉.
### Tables, Schemas & Data Types
When creating a new table to hold your data, you define a table schema telling
the database what columns there are, what kind of data they contain, and if
there are any constraints on your data. For example[^1],
```sql
CREATE TABLE post (
id INTEGER PRIMARY KEY,
created INTEGER NOT NULL DEFAULT (UNIXEPOCH()),
author INTEGER NOT NULL REFERENCES _user ON DELETE CASCADE,
title TEXT NOT NULL,
body TEXT NOT NULL
);
```
This creates a table to hold posts in a blog storing an integer creation
timestamp, the author, the title and lastly the contents.
We also set it up such that an author deleting their account, will delete their
posts as well through cascading deletions.
Coming from other SQL databases, it may come as a surprise that despite the
data types above SQLite isn't strictly typed by default, e.g. the `title`
column above may hold values other than `TEXT`.
SQLite interprets types merely as *affinities* to judge how literals should be
interpreted on insert or or update but will happily accept incompatible values.
While "flexible", this has far reaching consequences on downstream code
interpreting or transforming data, now having to explicitly deal with
unexpected data types.
Thus, we strongly encourage working with `STRICT` table schemas whenever
possible.
In fact, TrailBase APIs are type-safe and thus require the underlying tables to
be `STRICT`, i.e.:
```sql {4}
CREATE TABLE post (
id INTEGER PRIMARY KEY,
-- ...
) STRICT;
```
At the fundamental storage-level SQLite supports the following data types:
`NULL`, `INTEGER`, `REAL`, `TEXT`, `BLOB`, which all type affinities boil down
to.
For example, columns with `JSON` or `JSONB` type affinities are stored as
`TEXT` or `BLOB`, respectively[^2].
When defining `STRICT` tables there's no affinity, limiting you to SQLite's
fundamental types.
However, TrailBase provides you with some extensions to enforce strict schema
compatibility on a sub-column-level, e.g.
```sql {3-4}
CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
any_json TEXT CHECK(is_json(any_json)),
my_json TEXT CHECK(jsonschema('MyRegisteredJsonSchema', my_json))
) STRICT;
```
### Constraints
Using TrailBase you can make use of any of SQLite's column and table
constraints. We've already encountered some of the former, e.g. `NOT NULL`,
`REFERENCES` or `CHECK`, which all *constrain* the values a column may
contain.
Similarly, one can define more complex table constraints constraining tuples of
values, e.g.
```sql {5}
CREATE TABLE fully_qualifed_ids (
prefix TEXT NOT NULL,
suffix TEXT NOT NULL,
UNIQUE (prefix, suffix)
) STRICT;
```
For a complete list of constraints, check out the
[SQLite manual](https://www.sqlite.org/lang_createtable.html).
### Generated Columns
SQLite's generated columns allow you to either materialized derived columns at
modification-time or compute values for virtual columns on the fly. Check out
[SQLite manual](https://www.sqlite.org/lang_createtable.html) for comprehensive
information.
## Relations
SQL typically distinguishes between three types of relations:
* 1:1 relations, e.g. each user has exactly one profile.
* 1:M or one-to-many relations, e.g. posts may have many comments but each
comment belongs to exactly one post.
* N:M or many-to-many relations, e.g. shoppers' wishlists can contain many
items, and each item can be in many wishlists.
In practice, **all relations are simply edges, i.e. tuples of the shape
`(parent id, child id)`**.
There's some freedom as to where these edges are stored. In case of 1:1 and 1:M
relations they can be denormalized into the child record effectively becoming
`(primary key, foreign key)`.
Alternatively, edges can always be stored in a separate "bridge" table as
`(foreign key, foreign key)` edge records.
In case of N:M relations, this is even necessary to achieve the required
cardinality.
This is not a limitation of TrailBase but rather common SQL practice.
Linking children to their parents individually[^2] via foreign keys exposes
their relationships to the database allowing `ON DELETE` and `ON UPDATE`
actions to propagate.
For example, a user deletion may trigger related data to be deleted
automatically.
<Aside type="note" title="PocketBase">
If you're coming from PocketBase, 1:M and N:M relations are modeled as
denormalized lists of primary keys in JSON format.
While this *adjacency list* approach may feel more intuitive, it is opaque to
SQLite and thus breaking built-in foreign key support.
</Aside>
Let's look at the following *blog* example,
<div class="flex justify-center">
<div class="max-w-[420px]">
<Image src={foo} alt="Blog relationship example" />
</div>
</div>
Each block represents a able schema. We can see:
* a 1:1 relation between users and user profiles,
* a 1:M relationship between posts and users,
* and an N:M relationship between posts and tags using the `post_tag` bridge table.
We could have implemented the 1:1 user-profile and 1:M user-post relationships
via separate bridge tables with appropriate uniqueness constraints, however
pulling the parent key into the child records leads to less indirection.
In order to combine related data we can simply join on the keys. For example to
get a list of all users with profiles:
```sql
SELECT * FROM _user AS U
JOIN profile AS P ON U.id = P.user;
```
To connect posts and tags we have to do two joins across the `post_tag` bridge
table:
```sql
SELECT * FROM post AS P
LEFT JOIN post_tag AS PT ON P.id = PT.post
LEFT JOIN tag AS T ON T.ID = PT.tag;
```
### Record APIs
There's an elephant in the room: while this is all pretty standard fair, at
least in SQL land, connecting relations across TrailBase's record APIs without
joins is a lot more painful.
For N:M relations, one would have to expose 3 different APIs including the
bridge table, manage their ACLs and then traverse the edges manually on the
client side.
This is an area where we're actively exploring how to expand API capabilities,
however the most general approach is to push more responsibility to the
server.
Concretely, we can expose a single API tailored for a specific client use-case
implementing the join on the server using `VIEW`s:
```sql
CREATE VIEW post_tag_view AS SELECT * FROM post AS P
LEFT JOIN post_tag AS PT ON P.id = PT.post
LEFT JOIN tag AS T ON T.ID = PT.tag;
```
More generally, views can be useful to decouple an API from the underlying data
model.
For example, you may want to restructure your data model or APIs while keeping
the other stable.
Alternatively, custom JS/TS handlers can provide a more free-form approach to
push joining edges and other responsibilities to the server.
<div class="h-[50px]" />
---
[^1]:
This is not meant as full SQL tutorial. We'll keep it simple so hopefully
it is clear from context what any statement intends to do.
Note further that SQL keywords are case-insensitive. We'll use all-caps to
highlight them.
[^2]:
This also means than one can use SQLite's builtin JSON operators on any
TEXT column as long as it parses as JSON.
[^3]:
Even feature rich SQL databases like Postgres
[do not support foreign key arrays](https://commitfest.postgresql.org/17/1252/)
natively and rely on separate relationship tables.