tarantool-schema 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +47 -11
- data/lib/tarantool/schema/comparison.rb +11 -3
- data/lib/tarantool/schema/creation.rb +15 -0
- data/lib/tarantool/schema/definition.rb +34 -9
- data/lib/tarantool/schema/grant_comparison.rb +29 -0
- data/lib/tarantool/schema/grant_inspector.rb +38 -0
- data/lib/tarantool/schema/grant_spec.rb +28 -0
- data/lib/tarantool/schema/live_definition.rb +5 -0
- data/lib/tarantool/schema/plan.rb +17 -6
- data/lib/tarantool/schema/privileges.rb +31 -0
- data/lib/tarantool/schema/source.rb +12 -2
- data/lib/tarantool/schema/version.rb +1 -1
- data/lib/tarantool/schema.rb +5 -1
- data/sig/tarantool/schema.rbs +47 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: daa090a4546c7ecc8ed5c5e62f1a465b72e828c434e34076411069b6967a1914
|
|
4
|
+
data.tar.gz: 87f6b699e61c2e935f2dd508413910b89d333a1d302715bad446d9f5c5e90dbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c15dc87c94a1f8e24a537d53b63c84807e49436e34e4a24e1991184fed86d6f4c3abd0c40b6f6f5eb430762cc5954def2dea763f872e987fe459ac6635b0cc05
|
|
7
|
+
data.tar.gz: 85e8b8b6adfad1f6d9e61099991df5ee3001d0aa6c84e436a6c74faa5f1c38834536ff659ade1ef38d279fc1d2f89863f5557cc0ef6a005eb59b564b1ed30928
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
- `grant user, privileges, on: space` declares the privileges a user or role must have on a space. `plan` and
|
|
6
|
+
`apply` grant what is missing after the space and its indexes, keep extra privileges, never revoke, and raise
|
|
7
|
+
`MismatchError` for a grantee that does not exist. `from_live` reads grants, `to_ruby` renders them,
|
|
8
|
+
`Definition#grants` lists them. The deploy user needs `read,write` on `_priv` and `read` on `_user` for this.
|
|
9
|
+
|
|
3
10
|
## 0.2.1 (2026-10-17)
|
|
4
11
|
|
|
5
12
|
- Depends on `tarantool-client ~> 0.3`.
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# tarantool-schema
|
|
2
2
|
|
|
3
|
-
Idempotent schema declaration for Tarantool 2.11+ and 3.x, meant for deploys. You declare spaces, indexes
|
|
4
|
-
primary key sequences in Ruby; `apply` creates whatever is missing and checks everything that already
|
|
3
|
+
Idempotent schema declaration for Tarantool 2.11+ and 3.x, meant for deploys. You declare spaces, indexes,
|
|
4
|
+
primary key sequences and grants in Ruby; `apply` creates whatever is missing and checks everything that already
|
|
5
|
+
exists.
|
|
5
6
|
If the live schema differs from the declaration, it raises an error with a diff and creates nothing. It alters
|
|
6
7
|
only what you allow (appending nullable fields) and never drops anything: changing a field type or rebuilding an
|
|
7
8
|
index is a migration and is out of scope. An existing database can be read back into the DSL with `from_live`.
|
|
@@ -35,6 +36,8 @@ module MyApp
|
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
ensure_index :codes, :by_expiry, parts: [:expires_at], unique: false
|
|
39
|
+
|
|
40
|
+
grant :app, %i[read write], on: :codes
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
```
|
|
@@ -51,12 +54,14 @@ The block runs with the definition as `self`, then the definition is frozen: cal
|
|
|
51
54
|
- `ensure_index(space, name, **options)` adds an index to a space declared in the same definition. For a space
|
|
52
55
|
owned by someone else it only adds the index: the space must already exist, and part types come from its
|
|
53
56
|
live format.
|
|
57
|
+
- `grant(user, privileges, on:)` declares the privileges a user or a role must have on a space, see Grants.
|
|
54
58
|
|
|
55
59
|
### Validation
|
|
56
60
|
|
|
57
61
|
These raise `ArgumentError` while the definition is built, before anything reaches the server:
|
|
58
62
|
|
|
59
|
-
- a space declared twice, or declared after an `ensure_index` for it; a duplicate field or index name;
|
|
63
|
+
- a space declared twice, or declared after an `ensure_index` for it; a duplicate field or index name; the same
|
|
64
|
+
grantee twice on one space;
|
|
60
65
|
- unknown keys anywhere, including the unsupported options listed below;
|
|
61
66
|
- type aliases or unknown types. Only canonical names are accepted: `any unsigned string integer number double
|
|
62
67
|
boolean decimal uuid datetime interval varbinary scalar array map`. Use `string`, not `str`;
|
|
@@ -75,7 +80,9 @@ Declaring any of these raises `ArgumentError`, so nobody assumes they were appli
|
|
|
75
80
|
- field: `default`, `default_func`, `constraint`, `foreign_key`, `collation`;
|
|
76
81
|
- index: `id`, `hint`, `func`, `dimension`, `distance`, vinyl tuning (`page_size`, `range_size`,
|
|
77
82
|
`run_count_per_level`, `run_size_ratio`, `bloom_fpr`);
|
|
78
|
-
- part: `collation`, `sort_order`, `exclude_null
|
|
83
|
+
- part: `collation`, `sort_order`, `exclude_null`;
|
|
84
|
+
- grant: any privilege but `read write usage create drop alter trigger insert update delete` (`execute` on a
|
|
85
|
+
space exists on 2.11 only).
|
|
79
86
|
|
|
80
87
|
Options that are set on a live space but that you did not declare are ignored when comparing.
|
|
81
88
|
|
|
@@ -108,7 +115,7 @@ where it left off.
|
|
|
108
115
|
Comparison is strict about what you declared: engine; format (field count, and for each position the name,
|
|
109
116
|
type and nullability); the primary index name; and for each declared index its type, uniqueness, sequence and
|
|
110
117
|
parts. Indexes in the live space that you did not declare are ignored. Type aliases in a live format (`str`,
|
|
111
|
-
`num`) count as their canonical names.
|
|
118
|
+
`num`) count as their canonical names. Grants are compared as described under Grants.
|
|
112
119
|
|
|
113
120
|
```
|
|
114
121
|
schema mismatch:
|
|
@@ -141,6 +148,26 @@ Everything else stays a mismatch: a field that is not nullable (`field 4 flag: m
|
|
|
141
148
|
added`), a live format longer than the declared one, a field differing inside the prefix. Nothing is ever
|
|
142
149
|
dropped.
|
|
143
150
|
|
|
151
|
+
## Grants
|
|
152
|
+
|
|
153
|
+
Privileges on spaces belong in the schema: in Tarantool 3.x the YAML configuration grants them only to spaces
|
|
154
|
+
that exist when the configuration is applied, so a space created by a later deploy is left without. A grant
|
|
155
|
+
names a user or a role, the privileges and the space:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
grant :app, %i[read write], on: :codes
|
|
159
|
+
grant :reporting, :read, on: :codes
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`plan` and `apply` treat a grant like an index: after the space and its indexes they grant what is missing,
|
|
163
|
+
only the privileges still lacking (`grant app write on codes` when `read` is already there). Privileges granted
|
|
164
|
+
but not declared are kept, nothing is ever revoked. A grantee that does not exist is a mismatch (`grant app: no
|
|
165
|
+
such user or role`), raised before anything is created; users and roles are not managed here. A grant on a space
|
|
166
|
+
that is neither declared nor live is a mismatch too (`missing`). Privileges: `read write usage create drop alter
|
|
167
|
+
trigger insert update delete`.
|
|
168
|
+
|
|
169
|
+
The deploy user needs `read,write` on `_priv` and `read` on `_user` for grants, see Privileges.
|
|
170
|
+
|
|
144
171
|
## Bootstrapping from a live schema
|
|
145
172
|
|
|
146
173
|
```ruby
|
|
@@ -153,14 +180,16 @@ File.write('config/tarantool_schema.rb', definition.to_ruby)
|
|
|
153
180
|
`from_live` reads the spaces with ids past the system range whose names do not start with `_` (vshard's
|
|
154
181
|
`_bucket` and crud's `_ddl_*` stay out); `only:` names the spaces to read instead, with or without a leading
|
|
155
182
|
underscore, and raises `ArgumentError` for a name that does not exist. Each space is read as `ensure_space`
|
|
156
|
-
would declare it: engine, format, indexes in id order with their parts
|
|
183
|
+
would declare it: engine, format, indexes in id order with their parts, then the grants on it (a privilege the
|
|
184
|
+
DSL does not know, `execute` on 2.11, is left out). A part that inherits its type and
|
|
157
185
|
nullability from the field is a bare field name; a JSON path part keeps its type. Index options the DSL does not
|
|
158
186
|
know (`hint`, collations, `exclude_null`, vinyl tuning) are not read, as `plan` ignores them anyway. A
|
|
159
187
|
functional index, or a part pointing past the format, raises `ArgumentError` naming the space and index: the
|
|
160
188
|
DSL cannot express them.
|
|
161
189
|
|
|
162
190
|
`to_ruby` renders a definition as source for a `Tarantool::Schema::Definition.new do ... end` block, defaults
|
|
163
|
-
left out, one space per `ensure_space`, `ensure_index` entries after them
|
|
191
|
+
left out, one space per `ensure_space`, `ensure_index` entries after them, then one `grant` per line; it
|
|
192
|
+
evaluates back to the same `to_h` and `grants`:
|
|
164
193
|
|
|
165
194
|
```ruby
|
|
166
195
|
Tarantool::Schema::Definition.new do
|
|
@@ -175,6 +204,8 @@ Tarantool::Schema::Definition.new do
|
|
|
175
204
|
{ name: :by_code, parts: [:code] },
|
|
176
205
|
{ name: :by_expiry, parts: [:expires_at], unique: false }
|
|
177
206
|
]
|
|
207
|
+
|
|
208
|
+
grant :app, %i[read write], on: :codes
|
|
178
209
|
end
|
|
179
210
|
```
|
|
180
211
|
|
|
@@ -215,15 +246,17 @@ The deploy user needs these privileges (tested on 2.11 and 3.x):
|
|
|
215
246
|
| create spaces | `create` on `space` | same |
|
|
216
247
|
| primary key sequences | `create` on `sequence` | same |
|
|
217
248
|
| system spaces | `read,write` on spaces `_space`, `_index`, `_sequence`, `_space_sequence` | same |
|
|
249
|
+
| grants | `read,write` on space `_priv`, `read` on space `_user` | same |
|
|
218
250
|
|
|
219
251
|
```lua
|
|
220
252
|
box.schema.user.create('deployer', {password = '...'})
|
|
221
253
|
box.schema.user.grant('deployer', 'execute', 'lua_eval') -- 2.11: 'execute', 'universe'
|
|
222
254
|
box.schema.user.grant('deployer', 'create', 'space')
|
|
223
255
|
box.schema.user.grant('deployer', 'create', 'sequence')
|
|
224
|
-
for _, name in ipairs({'_space', '_index', '_sequence', '_space_sequence'}) do
|
|
256
|
+
for _, name in ipairs({'_space', '_index', '_sequence', '_space_sequence', '_priv'}) do
|
|
225
257
|
box.schema.user.grant('deployer', 'read,write', 'space', name)
|
|
226
258
|
end
|
|
259
|
+
box.schema.user.grant('deployer', 'read', 'space', '_user')
|
|
227
260
|
```
|
|
228
261
|
|
|
229
262
|
On 2.11, only `admin` can grant privileges on `universe`; a `super` user cannot. Adding an index to a space
|
|
@@ -237,7 +270,8 @@ in, part types inherited, and `ensure_index` entries merged into their spaces. U
|
|
|
237
270
|
test double or any other representation of the schema from the same declaration used to deploy it.
|
|
238
271
|
|
|
239
272
|
Only spaces declared with `ensure_space` are in it: indexes added with `ensure_index` to a space declared
|
|
240
|
-
elsewhere have no spec here and are not included.
|
|
273
|
+
elsewhere have no spec here and are not included. Grants are not in it either; `Definition#grants` lists them
|
|
274
|
+
as `[{ user: :app, privileges: ['read', 'write'], space: :codes }]`.
|
|
241
275
|
|
|
242
276
|
```ruby
|
|
243
277
|
MyApp::SCHEMA.to_h
|
|
@@ -277,8 +311,10 @@ INTEGRATION=1 TARANTOOL_URI=tcp://app:secret@localhost:3302 bundle exec rspec
|
|
|
277
311
|
|
|
278
312
|
## Public API
|
|
279
313
|
|
|
280
|
-
`Tarantool::Schema::Definition` (`new`, `from_live`, `ensure_space`, `ensure_index`, `
|
|
281
|
-
`to_ruby`) and the error classes above.
|
|
314
|
+
`Tarantool::Schema::Definition` (`new`, `from_live`, `ensure_space`, `ensure_index`, `grant`, `plan`, `apply`,
|
|
315
|
+
`to_h`, `grants`, `to_ruby`) and the error classes above.
|
|
316
|
+
|
|
317
|
+
RBS signatures for this API ship with the gem in `sig/`.
|
|
282
318
|
|
|
283
319
|
Everything else, including anything tagged `@api private` in its source comment, is an implementation detail
|
|
284
320
|
and may change in any release.
|
|
@@ -4,12 +4,12 @@ module Tarantool
|
|
|
4
4
|
module Schema
|
|
5
5
|
# Differences between one space as declared and as it is live. For a space declared elsewhere (only its
|
|
6
6
|
# indexes are declared here) there is no spec: engine and format are not compared and index parts inherit
|
|
7
|
-
# from the live format.
|
|
7
|
+
# from the live format. Grants are compared only when declared, against what GrantInspector read.
|
|
8
8
|
# @api private
|
|
9
9
|
class Comparison
|
|
10
|
-
attr_reader :name, :space, :mismatches, :missing_indexes, :format_additions
|
|
10
|
+
attr_reader :name, :space, :mismatches, :missing_indexes, :format_additions, :missing_grants
|
|
11
11
|
|
|
12
|
-
def initialize(name:, indexes:, live:, space: nil, alter: [])
|
|
12
|
+
def initialize(name:, indexes:, live:, space: nil, alter: [], grants: [], live_grants: nil)
|
|
13
13
|
@name = name
|
|
14
14
|
@space = space
|
|
15
15
|
@live = live
|
|
@@ -18,6 +18,7 @@ module Tarantool
|
|
|
18
18
|
@missing_indexes = []
|
|
19
19
|
@format_additions = []
|
|
20
20
|
live ? compare(indexes) : absent(indexes)
|
|
21
|
+
compare_grants(grants, live_grants)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def missing_space? = @live.nil? && !@space.nil?
|
|
@@ -37,6 +38,13 @@ module Tarantool
|
|
|
37
38
|
indexes.each { |index| compare_index(index) }
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
def compare_grants(grants, live_grants)
|
|
42
|
+
comparison = GrantComparison.new(grants, live_grants)
|
|
43
|
+
@mismatches.concat(comparison.mismatches)
|
|
44
|
+
|
|
45
|
+
@missing_grants = comparison.missing
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
def compare_space
|
|
41
49
|
@mismatches << "engine: declared #{@space.engine}, actual #{@live.engine}" if @space.engine != @live.engine
|
|
42
50
|
format = FormatComparison.new(@space.format, @live.format, add_field: @alter.include?(:add_field))
|
|
@@ -21,6 +21,12 @@ module Tarantool
|
|
|
21
21
|
box.space[name]:format(format)
|
|
22
22
|
LUA
|
|
23
23
|
|
|
24
|
+
GRANT_LUA = <<~LUA
|
|
25
|
+
local grantee, privileges, space = ...
|
|
26
|
+
local grant = box.schema.user.exists(grantee) and box.schema.user.grant or box.schema.role.grant
|
|
27
|
+
grant(grantee, privileges, 'space', space, {if_not_exists = true})
|
|
28
|
+
LUA
|
|
29
|
+
|
|
24
30
|
Space = Data.define(:spec) do
|
|
25
31
|
def apply(client)
|
|
26
32
|
client.eval(SPACE_LUA, [spec.name.to_s, spec.to_lua.merge('if_not_exists' => true)])
|
|
@@ -45,6 +51,15 @@ module Tarantool
|
|
|
45
51
|
|
|
46
52
|
def to_s = "index #{space}.#{spec.name}"
|
|
47
53
|
end
|
|
54
|
+
|
|
55
|
+
# Only the privileges still missing go out; an overlap with what is already granted is not an error.
|
|
56
|
+
Grant = Data.define(:spec) do
|
|
57
|
+
def apply(client)
|
|
58
|
+
client.eval(GRANT_LUA, [spec.user.to_s, spec.privileges.join(','), spec.space.to_s])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s = spec.to_s
|
|
62
|
+
end
|
|
48
63
|
end
|
|
49
64
|
end
|
|
50
65
|
end
|
|
@@ -2,28 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
module Tarantool
|
|
4
4
|
module Schema
|
|
5
|
-
# Spaces and
|
|
6
|
-
# constant cannot grow when its file is loaded again. The block runs with the definition as self.
|
|
5
|
+
# Spaces, indexes and grants an application needs. Built once from a block and frozen, so a definition kept
|
|
6
|
+
# in a constant cannot grow when its file is loaded again. The block runs with the definition as self.
|
|
7
7
|
class Definition
|
|
8
8
|
def initialize(&block)
|
|
9
9
|
@spaces = {}
|
|
10
10
|
@external = {}
|
|
11
|
+
@grants = {}
|
|
11
12
|
instance_exec(&block) if block
|
|
12
13
|
@spaces.transform_values! { |space| SpaceRules.check(space.resolve) }.freeze
|
|
13
14
|
@external.each_value(&:freeze).freeze
|
|
15
|
+
@grants.each_value(&:freeze).freeze
|
|
14
16
|
freeze
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
# Every user space as the server has it, or only the named ones. A space the DSL
|
|
19
|
+
# Every user space as the server has it, with the grants on it, or only the named ones. A space the DSL
|
|
20
|
+
# cannot express raises.
|
|
18
21
|
def self.from_live(client, only: nil)
|
|
19
|
-
arguments = SpaceLister.call(client, only:).to_h
|
|
20
|
-
live = Inspector.call(client, name) || raise(ArgumentError, "space #{name}: not found")
|
|
22
|
+
arguments = SpaceLister.call(client, only:).to_h { |name| [name, live_arguments(client, name)] }
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
new do
|
|
25
|
+
arguments.each do |name, (space, grants)|
|
|
26
|
+
ensure_space(name, **space)
|
|
27
|
+
grants.each { |user, privileges| grant(user, privileges, on: name) }
|
|
28
|
+
end
|
|
23
29
|
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.live_arguments(client, name)
|
|
33
|
+
live = Inspector.call(client, name) || raise(ArgumentError, "space #{name}: not found")
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
[LiveDefinition.arguments(name, live), LiveDefinition.grants(GrantInspector.call(client, name, []))]
|
|
26
36
|
end
|
|
37
|
+
private_class_method :live_arguments
|
|
27
38
|
|
|
28
39
|
def ensure_space(name, format:, indexes:, engine: :memtx)
|
|
29
40
|
raise ArgumentError, 'space without name' if name.nil?
|
|
@@ -49,9 +60,23 @@ module Tarantool
|
|
|
49
60
|
nil
|
|
50
61
|
end
|
|
51
62
|
|
|
63
|
+
# user may be a role too. Privileges already granted are kept, nothing is revoked.
|
|
64
|
+
def grant(user, privileges, on:)
|
|
65
|
+
spec = GrantSpec.build(user, privileges, on:)
|
|
66
|
+
grants = @grants[spec.space] ||= []
|
|
67
|
+
if grants.any? { |declared| declared.user == spec.user }
|
|
68
|
+
raise ArgumentError, "grant #{spec.user} on #{spec.space} is already declared"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
grants << spec
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
52
75
|
def to_h = @spaces.transform_values(&:to_h)
|
|
53
76
|
|
|
54
|
-
def
|
|
77
|
+
def grants = @grants.values.flatten.map(&:to_h)
|
|
78
|
+
|
|
79
|
+
def to_ruby = Source.render(to_h, @external.transform_values { |indexes| indexes.map(&:to_h) }, grants)
|
|
55
80
|
|
|
56
81
|
# alter: lists the changes to live spaces that are allowed besides creating what is missing, see Alter.
|
|
57
82
|
def plan(client, alter: []) = build_plan(client, Alter.check(alter)).verify.creations.map(&:to_s)
|
|
@@ -69,7 +94,7 @@ module Tarantool
|
|
|
69
94
|
|
|
70
95
|
private
|
|
71
96
|
|
|
72
|
-
def build_plan(client, alter) = Plan.new(spaces: @spaces, external: @external, client:, alter:)
|
|
97
|
+
def build_plan(client, alter) = Plan.new(spaces: @spaces, external: @external, grants: @grants, client:, alter:)
|
|
73
98
|
|
|
74
99
|
# A space declared elsewhere already has its primary key, so an index added here never gets a sequence.
|
|
75
100
|
def add_external(space, index)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Schema
|
|
5
|
+
# Declared grants on one space against the live ones: what is still to grant, and grantees that do not
|
|
6
|
+
# exist. Privileges granted but not declared are ignored, nothing is ever revoked. For a space that does not
|
|
7
|
+
# exist yet the live masks are empty, so everything is to grant, and a missing grantee is still a mismatch.
|
|
8
|
+
# @api private
|
|
9
|
+
class GrantComparison
|
|
10
|
+
attr_reader :mismatches, :missing
|
|
11
|
+
|
|
12
|
+
def initialize(grants, live)
|
|
13
|
+
@mismatches = []
|
|
14
|
+
@missing = []
|
|
15
|
+
grants.each { |grant| compare(grant, live) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def compare(grant, live)
|
|
21
|
+
return @mismatches << "grant #{grant.user}: no such user or role" if live.missing.include?(grant.user.to_s)
|
|
22
|
+
|
|
23
|
+
lacking = grant.missing(live.masks.fetch(grant.user.to_s, 0))
|
|
24
|
+
|
|
25
|
+
@missing << lacking if lacking
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Schema
|
|
5
|
+
# Privilege masks on one space by grantee name, and which of the declared grantees do not exist. A grantee
|
|
6
|
+
# may be a user or a role. Reads _priv and _user, so the connection needs read on both.
|
|
7
|
+
# @api private
|
|
8
|
+
LiveGrants = Data.define(:masks, :missing)
|
|
9
|
+
|
|
10
|
+
# @api private
|
|
11
|
+
module GrantInspector
|
|
12
|
+
LUA = <<~LUA
|
|
13
|
+
local name, grantees = ...
|
|
14
|
+
local masks = setmetatable({}, {__serialize = 'map'})
|
|
15
|
+
local space = box.space[name]
|
|
16
|
+
if space ~= nil then
|
|
17
|
+
for _, row in box.space._priv.index.object:pairs({'space', space.id}) do
|
|
18
|
+
local grantee = box.space._user:get(row[2])
|
|
19
|
+
if grantee ~= nil then masks[grantee.name] = row[5] end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
local missing = setmetatable({}, {__serialize = 'seq'})
|
|
23
|
+
for _, grantee in ipairs(grantees) do
|
|
24
|
+
if not (box.schema.user.exists(grantee) or box.schema.role.exists(grantee)) then
|
|
25
|
+
table.insert(missing, grantee)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return {masks = masks, missing = missing}
|
|
29
|
+
LUA
|
|
30
|
+
|
|
31
|
+
def self.call(client, space, grantees)
|
|
32
|
+
raw = client.eval(LUA, [space.to_s, grantees.map(&:to_s)]).first
|
|
33
|
+
|
|
34
|
+
LiveGrants.new(masks: raw.fetch('masks'), missing: raw.fetch('missing'))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Schema
|
|
5
|
+
# Privileges one user or role must have on one space.
|
|
6
|
+
# @api private
|
|
7
|
+
GrantSpec = Data.define(:user, :privileges, :space) do
|
|
8
|
+
def self.build(user, privileges, on:)
|
|
9
|
+
raise ArgumentError, 'grant without user' if user.nil?
|
|
10
|
+
raise ArgumentError, "grant #{user} without space" if on.nil?
|
|
11
|
+
|
|
12
|
+
user = user.to_sym
|
|
13
|
+
space = on.to_sym
|
|
14
|
+
|
|
15
|
+
new(user:, privileges: Privileges.check(privileges, "grant #{user} on #{space}").freeze, space:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The grant that would complete a live mask, or nil when it already covers the declaration.
|
|
19
|
+
def missing(mask)
|
|
20
|
+
lacking = privileges - Privileges.names(mask)
|
|
21
|
+
|
|
22
|
+
lacking.empty? ? nil : with(privileges: lacking.freeze)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s = "grant #{user} #{privileges.join(',')} on #{space}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -15,6 +15,11 @@ module Tarantool
|
|
|
15
15
|
indexes: live.indexes.map { |index| index_arguments(name, index, fields) } }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Grants whose privileges the DSL does not know (execute on 2.11) are left out.
|
|
19
|
+
def grants(live)
|
|
20
|
+
live.masks.to_h { |user, mask| [user.to_sym, Privileges.names(mask)] }.reject { |_, names| names.empty? }
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
def index_arguments(space, index, fields)
|
|
19
24
|
raise ArgumentError, "space #{space}: index #{index.name} is functional, not supported" if index.func
|
|
20
25
|
|
|
@@ -5,10 +5,12 @@ module Tarantool
|
|
|
5
5
|
# The live state of everything a definition declares, compared with it, and what is left to create.
|
|
6
6
|
# @api private
|
|
7
7
|
class Plan
|
|
8
|
-
def initialize(spaces:, external:, client:, alter: [])
|
|
8
|
+
def initialize(spaces:, external:, client:, alter: [], grants: {})
|
|
9
9
|
@alter = alter
|
|
10
|
+
@grants = grants
|
|
10
11
|
@comparisons = spaces.values.map { |space| compare(client, space.name, space.indexes, space) } +
|
|
11
|
-
external.map { |name, indexes| compare(client, name, indexes) }
|
|
12
|
+
external.map { |name, indexes| compare(client, name, indexes) } +
|
|
13
|
+
granted_only(spaces, external).map { |name| compare(client, name, []) }
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
# complete: anything still missing is a mismatch too, used after apply has created it.
|
|
@@ -22,16 +24,20 @@ module Tarantool
|
|
|
22
24
|
self
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
# Per space: the space itself or its format change, then its indexes, so an index on a new field works
|
|
27
|
+
# Per space: the space itself or its format change, then its indexes, so an index on a new field works,
|
|
28
|
+
# then its grants.
|
|
26
29
|
def creations
|
|
27
30
|
@comparisons.flat_map do |comparison|
|
|
28
31
|
space_creation(comparison) +
|
|
29
|
-
comparison.missing_indexes.map { |index| Creation::Index.new(space: comparison.name, spec: index) }
|
|
32
|
+
comparison.missing_indexes.map { |index| Creation::Index.new(space: comparison.name, spec: index) } +
|
|
33
|
+
comparison.missing_grants.map { |grant| Creation::Grant.new(spec: grant) }
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
private
|
|
34
38
|
|
|
39
|
+
def granted_only(spaces, external) = @grants.keys - spaces.keys - external.keys
|
|
40
|
+
|
|
35
41
|
def space_creation(comparison)
|
|
36
42
|
return [Creation::Space.new(spec: comparison.space)] if comparison.missing_space?
|
|
37
43
|
return [] if comparison.format_additions.empty?
|
|
@@ -40,7 +46,11 @@ module Tarantool
|
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def compare(client, name, indexes, space = nil)
|
|
43
|
-
|
|
49
|
+
grants = @grants.fetch(name, [])
|
|
50
|
+
live_grants = GrantInspector.call(client, name, grants.map(&:user)) if grants.any?
|
|
51
|
+
|
|
52
|
+
Comparison.new(name:, indexes:, live: Inspector.call(client, name), space:, alter: @alter, grants:,
|
|
53
|
+
live_grants:)
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
def still_missing(comparison)
|
|
@@ -48,7 +58,8 @@ module Tarantool
|
|
|
48
58
|
|
|
49
59
|
format = comparison.format_additions.empty? ? [] : ['format: fields missing after apply']
|
|
50
60
|
|
|
51
|
-
format + comparison.missing_indexes.map { |index| "index #{index.name}: missing after apply" }
|
|
61
|
+
format + comparison.missing_indexes.map { |index| "index #{index.name}: missing after apply" } +
|
|
62
|
+
comparison.missing_grants.map { |grant| "grant #{grant.user}: missing after apply" }
|
|
52
63
|
end
|
|
53
64
|
end
|
|
54
65
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Schema
|
|
5
|
+
# Privileges a grant on a space may name, with their bits in _priv. execute is left out: 3.x rejects it on
|
|
6
|
+
# a space. Names are kept in bit order, the order box.schema.user.info prints them in.
|
|
7
|
+
# @api private
|
|
8
|
+
module Privileges
|
|
9
|
+
BITS = { 'read' => 1, 'write' => 2, 'usage' => 16, 'create' => 32, 'drop' => 64, 'alter' => 128,
|
|
10
|
+
'trigger' => 512, 'insert' => 1024, 'update' => 2048, 'delete' => 4096 }.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def check(list, context)
|
|
15
|
+
names = Array(list).map(&:to_s)
|
|
16
|
+
raise ArgumentError, "#{context}: no privileges" if names.empty?
|
|
17
|
+
|
|
18
|
+
unknown = names.reject { |name| BITS.key?(name) }
|
|
19
|
+
if unknown.any?
|
|
20
|
+
raise ArgumentError, "#{context}: unknown privilege #{describe(unknown)}; allowed: #{BITS.keys.join(', ')}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
BITS.keys & names
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def describe(names) = names.map { |name| name.to_sym.inspect }.join(', ')
|
|
27
|
+
|
|
28
|
+
def names(mask) = BITS.select { |_, bit| mask.anybits?(bit) }.keys
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,21 +5,31 @@ module Tarantool
|
|
|
5
5
|
# Ruby source of a definition, from its normalized Hash: a Definition.new block with one ensure_space per
|
|
6
6
|
# space and one ensure_index per index on a space declared elsewhere. Defaults are left out and a part that
|
|
7
7
|
# inherits everything from its field is a bare name, so the result reads like a hand-written declaration.
|
|
8
|
+
# Grants come last, one line each.
|
|
8
9
|
# @api private
|
|
9
10
|
module Source
|
|
10
11
|
WIDTH = 120
|
|
11
12
|
ARGUMENT_INDENT = ' ' * 15
|
|
12
13
|
ENTRY_INDENT = ' ' * 17
|
|
14
|
+
NEEDS_ESCAPING = /['\\]/
|
|
13
15
|
|
|
14
16
|
module_function
|
|
15
17
|
|
|
16
|
-
def render(spaces, external)
|
|
18
|
+
def render(spaces, external, grants = [])
|
|
17
19
|
entries = spaces.map { |name, space| space(name, space) }
|
|
18
20
|
entries += external.map { |name, indexes| indexes(name, indexes) }
|
|
21
|
+
entries << grants.map { |grant| grant(grant) }.join("\n") if grants.any?
|
|
19
22
|
|
|
20
23
|
"Tarantool::Schema::Definition.new do\n#{entries.join("\n\n")}#{"\n" if entries.any?}end\n"
|
|
21
24
|
end
|
|
22
25
|
|
|
26
|
+
def grant(grant)
|
|
27
|
+
privileges = grant[:privileges]
|
|
28
|
+
literal = privileges.one? ? symbol(privileges.first) : "%i[#{privileges.join(' ')}]"
|
|
29
|
+
|
|
30
|
+
" grant #{symbol(grant[:user])}, #{literal}, on: #{symbol(grant[:space])}"
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
def space(name, space)
|
|
24
34
|
fields = space[:format].to_h { |field| [field[:name], field] }
|
|
25
35
|
format = list('format', space[:format].map { |field| field(field) })
|
|
@@ -102,7 +112,7 @@ module Tarantool
|
|
|
102
112
|
|
|
103
113
|
def symbol(name) = name.to_sym.inspect
|
|
104
114
|
|
|
105
|
-
def string(value) = value.match?(
|
|
115
|
+
def string(value) = value.match?(NEEDS_ESCAPING) ? value.inspect : "'#{value}'"
|
|
106
116
|
end
|
|
107
117
|
end
|
|
108
118
|
end
|
data/lib/tarantool/schema.rb
CHANGED
|
@@ -13,6 +13,10 @@ require_relative 'schema/part_spec'
|
|
|
13
13
|
require_relative 'schema/index_spec'
|
|
14
14
|
require_relative 'schema/space_spec'
|
|
15
15
|
require_relative 'schema/space_rules'
|
|
16
|
+
require_relative 'schema/privileges'
|
|
17
|
+
require_relative 'schema/grant_spec'
|
|
18
|
+
require_relative 'schema/grant_inspector'
|
|
19
|
+
require_relative 'schema/grant_comparison'
|
|
16
20
|
require_relative 'schema/plan'
|
|
17
21
|
require_relative 'schema/definition'
|
|
18
22
|
require_relative 'schema/live_space'
|
|
@@ -26,7 +30,7 @@ require_relative 'schema/comparison'
|
|
|
26
30
|
require_relative 'schema/creation'
|
|
27
31
|
|
|
28
32
|
module Tarantool
|
|
29
|
-
# Idempotent declaration of spaces, indexes
|
|
33
|
+
# Idempotent declaration of spaces, indexes, primary key sequences and grants, applied over an admin connection.
|
|
30
34
|
module Schema
|
|
31
35
|
end
|
|
32
36
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
# Idempotent declaration of spaces, indexes and primary key sequences.
|
|
3
|
+
module Schema
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
type name = String | Symbol
|
|
7
|
+
# A format entry: { name:, type:, is_nullable: }; type omitted means any, is_nullable defaults to false.
|
|
8
|
+
type field = { name: name, ?type: name, ?is_nullable: bool }
|
|
9
|
+
# A part is a field name, or a Hash with the keys that differ from the field: type, is_nullable, path.
|
|
10
|
+
type part = name | { field: name, ?type: name, ?is_nullable: bool, ?path: String }
|
|
11
|
+
# An index declaration: { name:, parts:, unique: true, type: :tree, sequence: false }.
|
|
12
|
+
type index = { name: name, parts: Array[part], ?unique: bool, ?type: name, ?sequence: bool }
|
|
13
|
+
# Changes to live spaces that plan and apply may make: :add_field.
|
|
14
|
+
type alter = Array[Symbol | String]
|
|
15
|
+
|
|
16
|
+
# Tarantool::Client connected to the read-write instance, or anything else that evaluates Lua.
|
|
17
|
+
interface _Evaluator
|
|
18
|
+
def eval: (String expression, ?Array[untyped] arguments) -> Array[untyped]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Definition
|
|
22
|
+
# The block runs with the definition as self; the definition is frozen afterwards.
|
|
23
|
+
def initialize: () ?{ () [self: Definition] -> void } -> void
|
|
24
|
+
# Every user space as the server has it (ids past the system range, names without a leading underscore),
|
|
25
|
+
# or only the named ones.
|
|
26
|
+
def self.from_live: (_Evaluator client, ?only: Array[name]?) -> Definition
|
|
27
|
+
def ensure_space: (name name, format: Array[field], indexes: Array[index], ?engine: name) -> nil
|
|
28
|
+
def ensure_index: (name space, name name, parts: Array[part], ?unique: bool, ?type: name,
|
|
29
|
+
?sequence: bool) -> nil
|
|
30
|
+
# The normalized declaration: names as Symbols, types as Strings, every default filled in.
|
|
31
|
+
def to_h: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
32
|
+
def to_ruby: () -> String
|
|
33
|
+
def plan: (_Evaluator client, ?alter: alter) -> Array[String]
|
|
34
|
+
def apply: (_Evaluator client, ?alter: alter) -> Array[String]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Error < Tarantool::Error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The live schema differs from the declaration; mismatches are the lines, each prefixed with the space.
|
|
41
|
+
class MismatchError < Error
|
|
42
|
+
attr_reader mismatches: Array[String]
|
|
43
|
+
|
|
44
|
+
def initialize: (Hash[name, Array[String]] by_space) -> void
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tarantool-schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Starlexxx
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: tarantool-client
|
|
@@ -44,6 +44,9 @@ files:
|
|
|
44
44
|
- lib/tarantool/schema/errors.rb
|
|
45
45
|
- lib/tarantool/schema/field_spec.rb
|
|
46
46
|
- lib/tarantool/schema/format_comparison.rb
|
|
47
|
+
- lib/tarantool/schema/grant_comparison.rb
|
|
48
|
+
- lib/tarantool/schema/grant_inspector.rb
|
|
49
|
+
- lib/tarantool/schema/grant_spec.rb
|
|
47
50
|
- lib/tarantool/schema/index_comparison.rb
|
|
48
51
|
- lib/tarantool/schema/index_spec.rb
|
|
49
52
|
- lib/tarantool/schema/inspector.rb
|
|
@@ -53,12 +56,14 @@ files:
|
|
|
53
56
|
- lib/tarantool/schema/options.rb
|
|
54
57
|
- lib/tarantool/schema/part_spec.rb
|
|
55
58
|
- lib/tarantool/schema/plan.rb
|
|
59
|
+
- lib/tarantool/schema/privileges.rb
|
|
56
60
|
- lib/tarantool/schema/source.rb
|
|
57
61
|
- lib/tarantool/schema/space_lister.rb
|
|
58
62
|
- lib/tarantool/schema/space_rules.rb
|
|
59
63
|
- lib/tarantool/schema/space_spec.rb
|
|
60
64
|
- lib/tarantool/schema/types.rb
|
|
61
65
|
- lib/tarantool/schema/version.rb
|
|
66
|
+
- sig/tarantool/schema.rbs
|
|
62
67
|
homepage: https://github.com/Starlexxx/tarantool-ruby
|
|
63
68
|
licenses:
|
|
64
69
|
- MIT
|