pg_graph 0.1.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.
data/lib/type/type.rb ADDED
@@ -0,0 +1,617 @@
1
+
2
+ module PgGraph::Type
3
+ class Error < StandardError; end
4
+
5
+ class Node < HashTree::Set
6
+ include Constrain
7
+
8
+ alias_method :uid, :path
9
+ def guid() root.name + "." + uid end
10
+
11
+ alias_method :name, :key
12
+
13
+ # The Ruby model identifier of the object. It corresponds to #name
14
+ def identifier() PgGraph.inflector.camelize(name) end
15
+
16
+ # The full Ruby model identifier of the object within a database. It
17
+ # corresponds to #uid
18
+ def schema_identifier() identifier end
19
+
20
+ # :call-seq: initialize(database, name, attach: true)
21
+ def initialize(parent, name, **opts)
22
+ constrain parent, Node, NilClass
23
+ constrain name, String, NilClass
24
+ super
25
+ end
26
+
27
+ def inspect(payload = inspect_inner)
28
+ "#<#{self.class}:#{name.inspect}#{payload ? " #{payload}" : ""}>"
29
+ end
30
+
31
+ def inspect_inner() nil end
32
+
33
+ protected
34
+ # Nodes with nil keys are not attached. This is used in PgCatalogSchema to
35
+ # avoid being included in the list of schemas
36
+ def do_attach(key, child) super if child end
37
+
38
+ # Forward list of methods to object. The arguments should be strings or symbols
39
+ def self.forward_method(object, *methods)
40
+ for method in Array(methods).flatten
41
+ class_eval("def #{method}(*args) #{object}.#{method}(*args) end")
42
+ end
43
+ end
44
+ end
45
+
46
+ # Included by tables, records, and fields
47
+ module TableObject
48
+ # Circular definitions. Classes that include TableObject should redefine at least
49
+ # one of the methods to break the loop
50
+ def table() record_type.table end
51
+ def table_type() table.type end
52
+ def record_type() table_type.record_type end
53
+ end
54
+
55
+ # Database is the top-level object and #read is the starting point for
56
+ # loading the type system in PgGraph::Type.new
57
+ class Database < Node
58
+ def guid() name end
59
+
60
+ alias_method :schemas, :values
61
+ attr_reader :catalog
62
+
63
+ # List of all tables in the database
64
+ def tables() schemas.map(&:tables).flatten end
65
+
66
+ # The reflector object. The reflector object takes a name of a link field
67
+ # and produce the field name in this table and the corrsponding field name
68
+ # in the other table
69
+ attr_reader :reflector
70
+
71
+ def initialize(name, reflector = PgGraph::Reflector.new)
72
+ constrain name, String
73
+ constrain reflector, PgGraph::Reflector
74
+ super(nil, name)
75
+ @catalog = PgCatalogSchema.new(self)
76
+ @reflector = reflector
77
+ end
78
+
79
+ def dot_lookup(key)
80
+ super || catalog[key]
81
+ end
82
+
83
+ # TODO
84
+ # def to_yaml() end
85
+ # def self.load_yaml() end
86
+
87
+ # Make Database pretend it is a PgGraph::Type object
88
+ def is_a?(klass) klass == PgGraph::Type || super end
89
+
90
+ def read(arg, reflector = nil, ignore: [])
91
+ constrain arg, PgMeta, PgConn
92
+ @reflector = reflector || @reflector
93
+ case arg
94
+ # #read_meta is a member of Database but defined in read.rb
95
+ when PgMeta; read_meta(arg, ignore: ignore)
96
+ when PgConn; read_meta(PgMeta.new(arg), ignore: ignore)
97
+ end
98
+ end
99
+ end
100
+
101
+ class Schema < Node
102
+ alias_method :database, :parent
103
+ alias_method :objects, :values
104
+
105
+ def tables() objects.select { |child| child.is_a?(Table) } end
106
+ def types() objects.select { |child| child.is_a?(Type) } end
107
+ def record_types() objects.select { |child| child.is_a?(RecordType) } end
108
+
109
+ # :call-seq: initialize(database, name, attach: true)
110
+ #
111
+ # +attach:false+ is used when initializing the pg_catalog schema object
112
+ def initialize(database, name, **opts)
113
+ constrain database, Database
114
+ super(database, name, **opts)
115
+ end
116
+ end
117
+
118
+ class PgCatalogSchema < Schema
119
+ def schema_identifier() nil end
120
+ def initialize(database) super(database, "pg_catalog", attach: false) end
121
+ end
122
+
123
+ # Schema objects are owned by the schema. This includes tables and
124
+ # user-defined types but not records that are owned by their tables
125
+ class SchemaObject < Node
126
+ alias_method :schema, :parent
127
+
128
+ def initialize(schema, name)
129
+ constrain schema, Schema
130
+ super
131
+ end
132
+
133
+ def schema_identifier
134
+ [schema.schema_identifier, identifier].compact.join("::")
135
+ end
136
+ end
137
+
138
+ # A table. Note that a table doesn't have child objects. Instead it has a type
139
+ # property that has the record type as child
140
+ class Table < SchemaObject
141
+ include TableObject
142
+
143
+ # Qualified name of table (ie. schema.table). FIXME: We already have Node#path???
144
+ attr_reader :path
145
+
146
+ # Table type. Initially nil. Initialized by RecordType.new
147
+ attr_reader :type
148
+ alias_method :table_type, :type # Required TableObject method
149
+
150
+ forward_method :type, :keys, :fields, :key?, :[], :columns, :value_columns, :column?
151
+
152
+ # Parent table if table is a derived table and otherwise nil. Initialized by #read
153
+ attr_reader :supertable
154
+
155
+ # True if table is a parent table. Initialized by #read
156
+ def supertable?() @has_subtables || false end
157
+
158
+ # True if table is a derived table
159
+ def subtable?() !@supertable.nil? end
160
+
161
+ def mm_table?() @mm_table end
162
+ def nm_table?() @nm_table end
163
+
164
+ # Array of tables in the transitive closure of table dependencies. It is
165
+ # initialized by Database#read_meta
166
+ attr_reader :depending_tables
167
+
168
+ # This is a hack since graph_db doesn't model views. It is needed because
169
+ # PgGraph::Data needs to know which materialized views to refresh after
170
+ # data has been loaded
171
+ attr_reader :depending_materialized_views
172
+
173
+ def initialize(
174
+ schema, name,
175
+ mm_table: false, nm_table: false, depending_materialized_views: [])
176
+ PgGraph.inflector.plural?(name) or raise Error, "Table names should be plural: #{name.inspect}"
177
+ super(schema, name)
178
+ @path = "#{schema.name}.#{name}"
179
+ @mm_table = mm_table || nm_table
180
+ @nm_table = nm_table
181
+ @depending_tables = []
182
+ @depending_materialized_views = depending_materialized_views
183
+ end
184
+
185
+ protected
186
+ def type=(t) @type = t end
187
+ end
188
+
189
+ # A type
190
+ class Type < SchemaObject
191
+ def rank() raise Error, "Abstract method" end
192
+ def array?() false end
193
+ def tuple?() false end
194
+ def value?() false end
195
+
196
+ def initialize(schema, name)
197
+ super(schema, name)
198
+ end
199
+
200
+ # Return array type of self. Schema defaults to the schema of self
201
+ def array_type(schema = self.schema)
202
+ array_name = PgGraph.inflector.type2array(name)
203
+ @array_type ||= schema[array_name] || ArrayType.new(schema, nil, self)
204
+ end
205
+ end
206
+
207
+ # TODO: Rename to CatalogType or PgType or PostgresType
208
+ class SimpleType < Type
209
+ alias_method :postgres_type, :key
210
+ attr_reader :ruby_class
211
+ def identifier() ruby_class.to_s end
212
+
213
+ def rank() 0 end
214
+ def value?() true end
215
+
216
+ def initialize(schema, postgres_type)
217
+ super(schema, postgres_type)
218
+ @ruby_class = PgGraph.inflector.postgres_type2ruby_class(postgres_type)
219
+ end
220
+ end
221
+
222
+ class CompositeType < Type
223
+ alias_method :fields, :values
224
+ def rank() 1 end
225
+ def tuple?() true end
226
+ end
227
+
228
+ class RecordType < CompositeType
229
+ include TableObject
230
+
231
+ # List of postgres columns. The columns are sorted by ordinal
232
+ def columns()
233
+ @columns ||= begin
234
+ cols = fields.map { |field|
235
+ case field
236
+ when SimpleColumn; field
237
+ when KindRecordColumn;
238
+ if field.kind_column&.parent
239
+ nil
240
+ else
241
+ field.kind_column
242
+ end
243
+ else
244
+ nil
245
+ end
246
+ }.compact.sort_by(&:ordinal)
247
+ # @columns_hash = @columns.map { |column| [column.name, column] }.to_h
248
+ cols
249
+ end
250
+ end
251
+
252
+ def postgres_columns()
253
+ @postgres_columns ||= begin
254
+ cols = fields.map { |field|
255
+ case field
256
+ when SimpleColumn; field
257
+ when KindRecordColumn;
258
+ if field.kind_column&.parent
259
+ nil
260
+ else
261
+ field.kind_column
262
+ end
263
+ else
264
+ nil
265
+ end
266
+ }.compact.sort_by(&:ordinal)
267
+ # @columns_ hash = @columns.map { |column| [column.name, column] }.to_h
268
+ cols
269
+ end
270
+ end
271
+
272
+ # TODO: Rename #columns to #postgres_columns and #abstract_columns to #columns
273
+ def abstract_columns
274
+ @abstract_columns ||= fields.map { |field|
275
+ case field
276
+ when SimpleColumn; field
277
+ when KindRecordColumn;
278
+ field.kind_column if field.kind_column.parent.nil?
279
+ else
280
+ nil
281
+ end
282
+ }.compact.sort_by(&:ordinal)
283
+
284
+ end
285
+
286
+ # field.is_a?(SimpleColumn) || field.is_a?(KindReference}.sort_by(&:ordinal) end
287
+
288
+ # List of columns excluding generated fields
289
+ def value_columns()
290
+ columns.select { |column| !column.generated? }
291
+ end
292
+
293
+ # True iff name if the name of a postgres column
294
+ # def column?(name) self[name].is_a?(SimpleColumn) end
295
+ def column?(name)
296
+ raise "See the rename TODO above"
297
+ @columns_hash.key?(name)
298
+ end
299
+
300
+ # Associated Table object
301
+ attr_reader :table
302
+
303
+ # Redefine array_type
304
+ def array_type() table_type end
305
+
306
+ # A record type has the schema as parent. Note that a record is
307
+ # initialized with a Table object and not a TableType object
308
+ def initialize(table)
309
+ constrain table, Table
310
+ super(table.schema, PgGraph.inflector.table2record_type(table.name))
311
+ @table = table
312
+ table_type = TableType.new(table.schema, self) # FIXME
313
+ @table.send(:type=, table_type)
314
+ end
315
+
316
+ def attach(*args)
317
+ @columns = nil
318
+ super
319
+ end
320
+ end
321
+
322
+ class ArrayType < Type
323
+ DEFAULT_MAX_DIMENSIONS = 5
324
+
325
+ attr_reader :element_type
326
+ attr_reader :dimensions
327
+
328
+ def rank() 2 end
329
+ def array?() true end
330
+
331
+ def initialize(schema, name, element_type, dimensions = 1)
332
+ constrain element_type, Type
333
+ self.class < TableObject or !element_type.is_a?(TableObject) or raise "Illegal element type"
334
+ dimensions <= ArrayType.max_dimensions or raise Error, "Array dimension overflow"
335
+ super(schema, name || PgGraph.inflector.type2array(element_type.name))
336
+ @element_type = element_type
337
+ @dimensions = dimensions
338
+ end
339
+
340
+ def identifier
341
+ "[#{element_type.identifier}]"
342
+ end
343
+
344
+ def schema_identifier
345
+ "[#{element_type.schema_identifier}]"
346
+ end
347
+
348
+ private
349
+ @max_dimensions = DEFAULT_MAX_DIMENSIONS
350
+ def self.max_dimensions() @max_dimensions end
351
+ def self.max_dimensions=(max) @max_dimensions = max end
352
+ end
353
+
354
+ # Note that the name of a TableType object is the record name in brackets
355
+ # while the name of a Table object is the pluralized record name
356
+ class TableType < ArrayType
357
+ include TableObject
358
+
359
+ alias_method :record_type, :element_type
360
+ forward_method :record_type, :keys, :fields, :key?, :[], :columns, :value_columns, :column?
361
+
362
+ def array_type() raise Error, "Array of TableType is not allowed" end
363
+
364
+ def initialize(schema, record_type)
365
+ constrain record_type, RecordType
366
+ super(schema, nil, record_type)
367
+ end
368
+
369
+ def identifier
370
+ "{#{element_type.identifier}}"
371
+ end
372
+
373
+ def schema_identifier
374
+ "{#{element_type.schema_identifier}}"
375
+ end
376
+ end
377
+
378
+ class Field < Node
379
+ alias_method :composite_type, :parent
380
+ forward_method :type, :rank
381
+ attr_reader :type
382
+
383
+ def initialize(composite_type, name, type)
384
+ constrain composite_type, CompositeType, NilClass
385
+ self.class < TableObject or !type.is_a?(TableObject) or raise "Illegal field type"
386
+ super(composite_type, name)
387
+ @type = type
388
+ end
389
+
390
+ def identifier() name end
391
+
392
+ def schema_identifier
393
+ composite_type.schema_identifier + '.' + identifier
394
+ end
395
+ end
396
+
397
+ # A field in a RecordType
398
+ class Column < Field
399
+ include TableObject
400
+
401
+ alias_method :record_type, :parent
402
+
403
+ # nil for TableColumn and SubRecordColumn objects
404
+ attr_reader :postgres_column
405
+
406
+ # Return true/false or nil if postgres_column is nil
407
+ def primary_key?() @postgres_column && @primary_key end
408
+
409
+ # Return true/false or nil if postgres_column is an identity column.
410
+ # Sub-keys are primary keys but not identity keys
411
+ def identity?() @postgres_column && @identity end
412
+
413
+ # Return true/false or nil
414
+ def nullable?() @postgres_column && @nullable end
415
+
416
+ # Return true/false or nil
417
+ def unique?() @postgres_column && @unique end
418
+
419
+ # Return true/false or nil
420
+ def readonly?() @postgres_column && @readonly end
421
+
422
+ # Return true/false or nil
423
+ def generated?() @postgres_column && @generated end
424
+
425
+ # True if column is a reference
426
+ def reference?() @postgres_column && @reference end
427
+
428
+ # True if column is a kind reference
429
+ def kind?() @postgres_column && @kind end
430
+
431
+ protected
432
+ def initialize(
433
+ record_type, name, postgres_column, type,
434
+ primary_key: false, identity: false,
435
+ reference: false, kind: false,
436
+ nullable: true, unique: false, readonly: false, generated: false)
437
+ constrain record_type, RecordType, NilClass
438
+ constrain type, Type
439
+ super(record_type, name, type)
440
+ @postgres_column = postgres_column
441
+ @primary_key = primary_key
442
+ @identity = identity
443
+ @reference = reference
444
+ @kind = kind
445
+ @nullable = nullable
446
+ @unique = unique
447
+ @readonly = readonly
448
+ @generated = generated
449
+ end
450
+ end
451
+
452
+ # Note that this includes array columns (for now)
453
+ class SimpleColumn < Column
454
+ # Ordinal of column in the database
455
+ attr_reader :ordinal
456
+
457
+ forward_method :type, :postgres_type, :ruby_class
458
+
459
+ def initialize(record_type, name, postgres_column, type, **opts)
460
+ constrain type, Type
461
+ !type.is_a?(TableObject) or raise "Illegal field type: #{type.class}"
462
+ @ordinal = opts.delete(:ordinal)
463
+ super
464
+ end
465
+
466
+ # Return true if the ruby literal match the column type
467
+ def literal?(text) text.is_a?(ruby_class) end
468
+ end
469
+
470
+ class RecordColumn < Column
471
+ # Postgres column in the embedding record that links to the referenced
472
+ # record
473
+ attr_reader :this_link_column
474
+
475
+ # Postgres column in the referenced record that links to the embedding
476
+ # record
477
+ attr_reader :that_link_column
478
+
479
+ # TODO: Is postgres_column == this_link_column?
480
+ def initialize(record_type, name, postgres_column, type, this_link_column, that_link_column, **opts)
481
+ constrain type, RecordType, NilClass
482
+ super(record_type, name, postgres_column, type, reference: true, **opts)
483
+ @this_link_column = this_link_column
484
+ @that_link_column = that_link_column
485
+ end
486
+
487
+ forward_method :type, :keys, :fields, :key?, :[], :columns, :column?
488
+
489
+ # Redefine #dot_lookup to make #dot enter the target record
490
+ def dot_lookup(key) type[key] end
491
+ end
492
+
493
+ class KindRecordColumn < RecordColumn
494
+ # A SimpleColumn object. Note that it doesn't have a parent to avoid having
495
+ # two columns with the same name: This structured KindRecordColumn object
496
+ # and the underlying kind column
497
+ attr_reader :kind_column
498
+
499
+ forward_method :kind_column, :literal?
500
+
501
+ def initialize(
502
+ record_type, name, postgres_column, type,
503
+ this_link_column, that_link_column,
504
+ kind_column, **opts)
505
+ constrain kind_column, SimpleColumn
506
+ super(record_type, name, postgres_column, type, this_link_column, that_link_column, kind: true, **opts)
507
+ @kind_column = kind_column
508
+ end
509
+ end
510
+
511
+ # A record in a 1:1 relationship with another record linked by identical IDs.
512
+ # SuperRecordColumn is on the "child" side of the relation because a child
513
+ # has a super record - hence the name
514
+ class SuperRecordColumn < RecordColumn
515
+ def initialize(record_type, name = nil, type)
516
+ name ||= type.name
517
+ super(
518
+ record_type, name, "id", type, "id", "id",
519
+ primary_key: true, nullable: false, unique: true, readonly: true)
520
+ end
521
+ end
522
+
523
+ # SubRecordColumn is on the "parent" side of the relation because a a parent
524
+ # has a sub record - hence the name
525
+ class SubRecordColumn < RecordColumn
526
+ def initialize(record_type, name = nil, type)
527
+ name ||= type.name
528
+ super(
529
+ record_type, name, "id", type, "id", "id",
530
+ primary_key: nil, nullable: nil, unique: nil, readonly: nil)
531
+ end
532
+ end
533
+
534
+ class TableColumn < Column
535
+ # Postgres column in the embedding record that links (directly or
536
+ # indirectly) to the subject table. This is typically the id column
537
+ attr_reader :this_link_column
538
+
539
+ # Postgres column in the subject table that directly or indirectly links to
540
+ # the embedding record. This is typically the '<this-table>_id' column of
541
+ # that table in 1:N relations
542
+ attr_reader :that_link_column
543
+
544
+ forward_method :type, :keys, :fields, :key?, :[], :columns, :column?
545
+
546
+ def initialize(record_type, name, type, this_link_column, that_link_column, **opts)
547
+ constrain record_type, RecordType
548
+ constrain type, TableType
549
+ super(record_type, name, nil, type, **opts)
550
+ @this_link_column = this_link_column
551
+ @that_link_column = that_link_column
552
+ end
553
+
554
+ def dot_lookup(key) type[key] end
555
+ end
556
+
557
+ class MmTableColumn < TableColumn
558
+ # This table in the relationship
559
+ alias_method :this_table, :table
560
+ alias_method :this_table_type, :table_type
561
+ alias_method :this_record_type, :record_type
562
+
563
+ # The other table in the relationship
564
+ def that_table() that_table_type.table end
565
+ alias_method :that_table_type, :type
566
+ def that_record_type() that_table_type.record_type end
567
+
568
+ # The link table between the two tables
569
+ def mm_table() mm_table_type.table end
570
+ def mm_record_type() mm_table_type.record_type end
571
+ attr_reader :mm_table_type
572
+
573
+ # Postgres column in the NM table that links to this table. This is
574
+ # typically the '<this-table>_id' column
575
+ attr_reader :this_mm_column
576
+
577
+ # Postgres column in the NM table that links to the embedding record. This
578
+ # is typically the '<that-table>_id' column
579
+ attr_reader :that_mm_column
580
+
581
+ # :unique in this context means that the associated records of this column
582
+ # are unique
583
+ def initialize(
584
+ record_type, name, that_table_type, mm_table_type,
585
+ this_link_column, this_mm_column, that_mm_column, that_link_column,
586
+ **opts)
587
+
588
+ # puts "MmTableColumn#initialize"
589
+ # indent {
590
+ # puts "record: #{record_type}"
591
+ # puts "column: #{name}"
592
+ # puts this_mm_column
593
+ # puts this_link_column
594
+ # puts that_mm_column
595
+ # puts that_link_column
596
+ # }
597
+
598
+ constrain record_type, RecordType
599
+ constrain that_table_type, TableType
600
+ constrain mm_table_type, TableType
601
+ mm_table_type.table.mm_table? or raise Error, "Link table expected"
602
+ super(record_type, name, that_table_type, this_link_column, that_link_column, **opts)
603
+ @mm_table_type = mm_table_type
604
+ @this_mm_column = this_mm_column
605
+ @that_mm_column = that_mm_column
606
+ end
607
+ end
608
+
609
+ class NmTableColumn < MmTableColumn
610
+ alias_method :nm_table, :mm_table
611
+ alias_method :nm_record_type, :mm_record_type
612
+ alias_method :nm_table_type, :mm_table_type
613
+ alias_method :this_nm_column, :this_mm_column
614
+ alias_method :that_nm_column, :that_mm_column
615
+ end
616
+ end
617
+
data/pg_graph.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ require_relative 'lib/pg_graph/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "pg_graph"
5
+ spec.version = PgGraph::VERSION
6
+ spec.authors = ["Claus Rasmussen"]
7
+ spec.email = ["claus.l.rasmussen@gmail.com"]
8
+
9
+ spec.summary = %q{Create graph type model of database}
10
+ spec.description = %q{dwpg_graph gem}
11
+ spec.homepage = "http://www.nowhere.com/"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ # spec.add_dependency GEM [, VERSION]
27
+ spec.add_dependency "boolean"
28
+ spec.add_dependency "constrain"
29
+ spec.add_dependency "developer_exceptions"
30
+ spec.add_dependency "dry-inflector"
31
+ spec.add_dependency "hash_tree", "= 0.1.1"
32
+ spec.add_dependency "indented_io"
33
+ spec.add_dependency "shellopts", "2.0.6"
34
+
35
+ spec.add_dependency "pg_conn", "= 0.2.1"
36
+ spec.add_dependency "pg_meta", "0.1.0"
37
+
38
+ # Also un-comment in spec/spec_helper to use simplecov
39
+ # spec.add_development_dependency "simplecov"
40
+ end
data/snippets/1-1.sql ADDED
@@ -0,0 +1,19 @@
1
+
2
+ \connect postgres
3
+
4
+ drop database if exists pg_graph;
5
+ create database pg_graph;
6
+
7
+ \connect pg_graph
8
+
9
+ create table pictures (
10
+ id integer generated by default as identity primary key,
11
+ title text not null,
12
+ file text
13
+ );
14
+
15
+ create table blobs (
16
+ id integer not null references pictures(id) primary key,
17
+ blob text not null
18
+ );
19
+
data/snippets/N-M.sql ADDED
@@ -0,0 +1,24 @@
1
+
2
+ \connect postgres
3
+
4
+ drop database if exists pg_graph;
5
+ create database pg_graph;
6
+
7
+ \connect pg_graph
8
+
9
+ create table roles (
10
+ id integer generated by default as identity primary key,
11
+ name text not null
12
+ );
13
+
14
+ create table users (
15
+ id integer generated by default as identity primary key,
16
+ name text not null
17
+ );
18
+
19
+ create table user_roles (
20
+ id integer generated by default as identity primary key,
21
+ role_id integer not null references roles(id),
22
+ user_id integer not null references users(id)
23
+ );
24
+