Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update constraints and foreign keys docs #3583

Merged
merged 8 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions doc/code_snippets/test/constraints/constraint_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
local fio = require('fio')
local server = require('luatest.server')
local t = require('luatest')
local g = t.group()
g.before_each(function(cg)
cg.server = server:new {
box_cfg = {},
workdir = fio.cwd() .. '/tmp'
}
cg.server:start()
end)

g.after_each(function(cg)
cg.server:stop()
cg.server:drop()
end)

g.test_constraints = function(cg)
cg.server:exec(function()

-- Tuple constraint function --
box.schema.func.create('check_person',
{language = 'LUA', is_deterministic = true, body = 'function(t, c) return (t.age >= 0 and #(t.name) > 3) end'})
p7nov marked this conversation as resolved.
Show resolved Hide resolved

-- Field constraint function --
box.schema.func.create('check_age',
{language = 'LUA', is_deterministic = true, body = 'function(f, c) return (f >= 0 and f < 150) end'})

-- Create a space with tuple constraint --
customers = box.schema.space.create('customers', { engine = 'memtx', constraint = 'check_person'})

-- Specify format with a field constraint --
box.space.customers:format({
{name = 'id', type = 'number'},
p7nov marked this conversation as resolved.
Show resolved Hide resolved
{name = 'name', type = 'string'},
{name = 'age', type = 'number', constraint = 'check_age'},
})


box.space.customers:create_index('primary', { parts = { 1 } })

box.space.customers:insert{1, "Alice", 30}

-- Tests --
t.assert_equals(customers:count(), 1)
t.assert_equals(customers:get(1), {1, "Alice", 30})

-- Failed contstraint --
t.assert_error_msg_contains("Check constraint 'check_person' failed for tuple",
function() customers:insert{2, "Bob", 230} end)

box.schema.func.create('another_constraint',
{language = 'LUA', is_deterministic = true, body = 'function(t, c) return (t.age >= 0 and #(t.name) > 3) end'})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, include this code to docs to understand where another_constraint is taken from?


-- Set two constraints with optional names --
box.space.customers:alter{
constraint = { check1 = 'check_person', check2 = 'another_constraint'}
}
end)
end
56 changes: 56 additions & 0 deletions doc/code_snippets/test/foreign_keys/field_foreign_key_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
local fio = require('fio')
local server = require('luatest.server')
local t = require('luatest')
local g = t.group()
g.before_each(function(cg)
cg.server = server:new {
box_cfg = {},
workdir = fio.cwd() .. '/tmp'
}
cg.server:start()
end)

g.after_each(function(cg)
cg.server:stop()
cg.server:drop()
end)

g.test_foreign_keys = function(cg)
cg.server:exec(function()

-- Parent space --
customers = box.schema.space.create('customers')
customers:format{
{name = 'id', type = 'number'},
{name = 'name', type = 'string'}
}

customers:create_index('primary', { parts = { 1 } })
customers:create_index('name', { parts = { 2 } })
customers:create_index('id_name', { parts = { 1, 2 } })

customers:insert({1, 'Alice'})

-- Create a space with a field foreign key --
box.schema.space.create('orders')

box.space.orders:format({
{name = 'id', type = 'number'},
{name = 'customer_id', foreign_key = {space = 'customers', field = 'id'}},
{name = 'price_total', type = 'number'},
p7nov marked this conversation as resolved.
Show resolved Hide resolved
})

orders = box.space.orders
orders:create_index('primary', { parts = { 1 } })

orders:insert({1, 1, 100})

t.assert_equals(orders:count(), 1)
t.assert_equals(orders:get(1), {1, 1, 100})

-- Failed foreign key check --
t.assert_error_msg_contains("Foreign key constraint 'customers' failed for field '2 (customer_id)': foreign tuple was not found",
function() orders:insert{2, 10, 200} end)

end)
end
80 changes: 80 additions & 0 deletions doc/code_snippets/test/foreign_keys/tuple_foreign_key_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
local fio = require('fio')
local server = require('luatest.server')
local t = require('luatest')
local g = t.group()
g.before_each(function(cg)
cg.server = server:new {
box_cfg = {},
workdir = fio.cwd() .. '/tmp'
}
cg.server:start()
end)

g.after_each(function(cg)
cg.server:stop()
cg.server:drop()
end)

g.test_foreign_keys = function(cg)
cg.server:exec(function()

-- Parent space --
customers = box.schema.space.create('customers')
customers:format{
{name = 'id', type = 'number'},
{name = 'name', type = 'string'}
}

customers:create_index('primary', { parts = { 1 } })
customers:create_index('name', { parts = { 2 } })
customers:create_index('id_name', { parts = { 1, 2 } })

customers:insert({1, 'Alice'})

-- Create a space with a tuple foreign key --
box.schema.space.create("orders", {foreign_key={space='customers', field={customer_id='id', customer_name='name'}}})

box.space.orders:format({
{name = "id", type = "number"},
{name = "customer_id" },
{name = "customer_name"},
{name = "price_total", type = "number"},
})

orders = box.space.orders
orders:create_index('primary', { parts = { 1 } })

orders:insert({1, 1, 'Alice', 100})

t.assert_equals(orders:count(), 1)
t.assert_equals(orders:get(1), {1, 1, 'Alice', 100})

-- Failed foreign key check --
t.assert_error_msg_contains("Foreign key constraint 'customers' failed: foreign tuple was not found",
function() orders:insert{2, 10, 'Bob', 200} end)

-- Set a foreign key with an optional name --
box.space.orders:alter{
foreign_key = {customer = {space = 'customers', field={customer_id='id', customer_name='name'}}}
}

items = box.schema.space.create('items')
items:format{{name = 'id', type = 'number'}}
items:create_index('primary', { parts = { 1 } })

orders:delete{1}
orders:format({
{name = "id", type = "number"},
{name = "customer_id" },
{name = "customer_name"},
{name = "item_id", type = "number"},
{name = "price_total", type = "number"},
})

-- Set two foreign keys: names are mandatory --
box.space.orders:alter{
foreign_key = {customer = {space = 'customers', field = {customer_id = 'id', customer_name = 'name'}},
item = {space = 'items', field = {item_id = 'id'}}}
}
end)
end
117 changes: 56 additions & 61 deletions doc/concepts/data_model/value_store.rst
Original file line number Diff line number Diff line change
Expand Up @@ -606,23 +606,20 @@ To create a constraint function, use :ref:`func.create with function body <box_s

Constraint functions take two parameters:

* The field value and the constraint name for field constraints.
* The tuple and the constraint name for tuple constraints.

.. code-block:: tarantoolsession
.. literalinclude:: /code_snippets/test/constraints/constraint_test.lua
:language: lua
:lines: 21-23
:dedent:

tarantool> box.schema.func.create('check_age',
> {language = 'LUA', is_deterministic = true, body = 'function(f, c) return (f >= 0 and f < 150) end'})
---
...

* The tuple and the constraint name for tuple constraints.
* The field value and the constraint name for field constraints.

.. code-block:: tarantoolsession
.. literalinclude:: /code_snippets/test/constraints/constraint_test.lua
:language: lua
:lines: 25-27
:dedent:

tarantool> box.schema.func.create('check_person',
> {language = 'LUA', is_deterministic = true, body = 'function(t, c) return (t.age >= 0 and #(t.name) > 3) end'})
---
...

.. warning::

Expand All @@ -638,28 +635,27 @@ Creating constraints
To create a constraint in a space, specify the corresponding function's name
in the ``constraint`` parameter:

* Field constraints: when setting up the space format:

.. code-block:: tarantoolsession

tarantool> box.space.person:format({
> {name = 'id', type = 'number'},
> {name = 'name', type = 'string'},
> {name = 'age', type = 'number', constraint = 'check_age'},
> })

* Tuple constraints: when creating or altering a space:

.. code-block:: tarantoolsession
.. literalinclude:: /code_snippets/test/constraints/constraint_test.lua
:language: lua
:lines: 29-30
:dedent:

tarantool> box.schema.space.create('person', { engine = 'memtx', constraint = 'check_tuple'})
* Field constraints: when setting up the space format:

.. literalinclude:: /code_snippets/test/constraints/constraint_test.lua
:language: lua
:lines: 32-37
:dedent:

In both cases, ``constraint`` can contain multiple function names passed as a tuple.
Each constraint can have an optional name:

.. code-block:: lua

constraint = {'age_constraint' = 'check_age', 'name_constraint' = 'check_name'}
.. literalinclude:: /code_snippets/test/constraints/constraint_test.lua
:language: lua
:lines: 55-58
:dedent:

.. note::

Expand All @@ -673,22 +669,28 @@ Each constraint can have an optional name:
Foreign keys
------------

**Foreign keys** provide links between related spaces, therefore maintaining the
**Foreign keys** provide links between related fields, therefore maintaining the
`referential integrity <https://en.wikipedia.org/wiki/Referential_integrity>`_
of the database.

Some fields can only contain values present in other spaces. For example,
shop orders always belong to existing customers. Hence, all values of the ``customer``
field of the ``orders`` space must exist in the ``customers`` space. In this case,
``customers`` is a **parent space** for ``orders`` (its **child space**). When two
spaces are linked with a foreign key, each time a tuple is inserted or modified
in the child space, Tarantool checks that a corresponding value is present in
the parent space.

Some fields can only contain values that exist in other fields. For example,
p7nov marked this conversation as resolved.
Show resolved Hide resolved
a shop order always belongs to a customer. Hence, all values of the ``customer``
field of the ``orders`` space must also exist in the ``id`` field of the ``customers``
space. In this case, ``customers`` is a **parent space** for ``orders`` (its **child space**).
When two spaces are linked with a foreign key, each time a tuple is inserted or
modified in the child space, Tarantool checks that a corresponding value is present
in the parent space.

.. image:: foreign_key.svg
:align: center

.. note::

A foreign key can link a field to another field in the same space. In this case,
the child field must be nullable. Otherwise, it will be impossible to insert
p7nov marked this conversation as resolved.
Show resolved Hide resolved
the first tuple in such a space because there is no parent tuple to which
it can link.

Foreign key types
~~~~~~~~~~~~~~~~~

Expand All @@ -714,32 +716,23 @@ Creating foreign keys
For each foreign key, there must exist an index that includes all its fields.

To create a foreign key in a space, specify the parent space and linked fields in the ``foreign_key`` parameter.
Parent spaces can be referenced by name or by id. When linking to the same space, the space can be omitted.
Fields can be referenced by name or by number:

* Field foreign keys: when setting up the space format.

.. code-block:: tarantoolsession

tarantool> box.space.orders:format({
> {name = 'id', type = 'number'},
> {name = 'customer_id', foreign_key = {space = 'customers', field = 'id'}}, -- or field = 1
> {name = 'price_total', type = 'number'},
> })
.. literalinclude:: /code_snippets/test/foreign_keys/field_foreign_key_test.lua
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd include the index creation here - copying and pasting this sample causes an error now: error: 'No index #0 is defined in space ''orders''. Maybe, it's worth including the code showing how to create the customers space.

Copy link
Contributor Author

@p7nov p7nov Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including the index without the parent space creation and formatting doesn't make this snippet copy-pastable.
On the other hand, including everything about the parent space (creation, format, indexes) doubles the snippet size, making the mechanism look heavy and complex.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, I agree. As an idea for the future, it would be convenient to have smth like the Open full sample button in the right corner to have the capability to open the example right on the GithHub and understand the full context.

:language: lua
:lines: 34-41
:dedent:

* Tuple foreign keys: when creating or altering a space. Note that for foreign
keys with multiple fields there must exist an index that includes all these fields.

.. code-block:: tarantoolsession

tarantool> box.schema.space.create("orders", {foreign_key={space='customers', field={customer_id='id', customer_name='name'}}})
---
...
tarantool> box.space.orders:format({
> {name = "id", type = "number"},
> {name = "customer_id" },
> {name = "customer_name"},
> {name = "price_total", type = "number"},
> })
.. literalinclude:: /code_snippets/test/foreign_keys/tuple_foreign_key_test.lua
:language: lua
:lines: 34-42
:dedent:

.. note::

Expand All @@ -748,15 +741,17 @@ Fields can be referenced by name or by number:

Foreign keys can have an optional name.

.. code-block:: lua

foreign_key = {customer = {space = '...', field = {...}}}
.. literalinclude:: /code_snippets/test/foreign_keys/tuple_foreign_key_test.lua
:language: lua
:lines: 56-59
:dedent:

A space can have multiple tuple foreign keys. In this case, they all must have names.

.. code-block:: lua

foreign_key = {customer = {space = '...', field = {...} }, item = { space = '...', field = {...}}}
.. literalinclude:: /code_snippets/test/foreign_keys/tuple_foreign_key_test.lua
:language: lua
:lines: 74-78
:dedent:

Tarantool performs integrity checks upon data modifications in parent spaces.
If you try to remove a tuple referenced by a foreign key or an entire parent space,
Expand Down
Loading