rom-sql 3.0.0 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +425 -296
  3. data/LICENSE +20 -0
  4. data/README.md +14 -55
  5. data/lib/rom-sql.rb +2 -0
  6. data/lib/rom/plugins/relation/sql/auto_restrictions.rb +2 -0
  7. data/lib/rom/plugins/relation/sql/instrumentation.rb +2 -0
  8. data/lib/rom/plugins/relation/sql/postgres/explain.rb +6 -7
  9. data/lib/rom/plugins/relation/sql/postgres/full_text_search.rb +53 -0
  10. data/lib/rom/plugins/relation/sql/postgres/streaming.rb +97 -0
  11. data/lib/rom/sql.rb +2 -0
  12. data/lib/rom/sql/associations.rb +2 -0
  13. data/lib/rom/sql/associations/core.rb +10 -0
  14. data/lib/rom/sql/associations/many_to_many.rb +3 -1
  15. data/lib/rom/sql/associations/many_to_one.rb +2 -0
  16. data/lib/rom/sql/associations/one_to_many.rb +2 -0
  17. data/lib/rom/sql/associations/one_to_one.rb +2 -0
  18. data/lib/rom/sql/associations/one_to_one_through.rb +2 -0
  19. data/lib/rom/sql/associations/self_ref.rb +2 -0
  20. data/lib/rom/sql/attribute.rb +31 -22
  21. data/lib/rom/sql/attribute_aliasing.rb +88 -0
  22. data/lib/rom/sql/attribute_wrapping.rb +30 -0
  23. data/lib/rom/sql/commands.rb +2 -0
  24. data/lib/rom/sql/commands/create.rb +2 -0
  25. data/lib/rom/sql/commands/delete.rb +2 -0
  26. data/lib/rom/sql/commands/error_wrapper.rb +2 -0
  27. data/lib/rom/sql/commands/update.rb +2 -0
  28. data/lib/rom/sql/dsl.rb +11 -3
  29. data/lib/rom/sql/error.rb +2 -0
  30. data/lib/rom/sql/errors.rb +2 -0
  31. data/lib/rom/sql/extensions.rb +2 -0
  32. data/lib/rom/sql/extensions/active_support_notifications.rb +2 -0
  33. data/lib/rom/sql/extensions/mysql.rb +2 -0
  34. data/lib/rom/sql/extensions/mysql/type_builder.rb +2 -0
  35. data/lib/rom/sql/extensions/postgres.rb +3 -0
  36. data/lib/rom/sql/extensions/postgres/commands.rb +3 -1
  37. data/lib/rom/sql/extensions/postgres/type_builder.rb +6 -4
  38. data/lib/rom/sql/extensions/postgres/type_serializer.rb +2 -0
  39. data/lib/rom/sql/extensions/postgres/types.rb +2 -0
  40. data/lib/rom/sql/extensions/postgres/types/array.rb +7 -6
  41. data/lib/rom/sql/extensions/postgres/types/array_types.rb +3 -1
  42. data/lib/rom/sql/extensions/postgres/types/geometric.rb +2 -0
  43. data/lib/rom/sql/extensions/postgres/types/json.rb +76 -19
  44. data/lib/rom/sql/extensions/postgres/types/ltree.rb +25 -23
  45. data/lib/rom/sql/extensions/postgres/types/network.rb +2 -0
  46. data/lib/rom/sql/extensions/postgres/types/range.rb +2 -0
  47. data/lib/rom/sql/extensions/rails_log_subscriber.rb +2 -0
  48. data/lib/rom/sql/extensions/sqlite.rb +2 -0
  49. data/lib/rom/sql/extensions/sqlite/type_builder.rb +2 -0
  50. data/lib/rom/sql/extensions/sqlite/types.rb +2 -0
  51. data/lib/rom/sql/foreign_key.rb +3 -1
  52. data/lib/rom/sql/function.rb +30 -3
  53. data/lib/rom/sql/gateway.rb +9 -1
  54. data/lib/rom/sql/group_dsl.rb +2 -0
  55. data/lib/rom/sql/index.rb +2 -0
  56. data/lib/rom/sql/join_dsl.rb +2 -0
  57. data/lib/rom/sql/mapper_compiler.rb +12 -1
  58. data/lib/rom/sql/migration.rb +5 -3
  59. data/lib/rom/sql/migration/inline_runner.rb +2 -0
  60. data/lib/rom/sql/migration/migrator.rb +4 -2
  61. data/lib/rom/sql/migration/recorder.rb +2 -0
  62. data/lib/rom/sql/migration/runner.rb +4 -2
  63. data/lib/rom/sql/migration/schema_diff.rb +2 -0
  64. data/lib/rom/sql/migration/template.rb +2 -0
  65. data/lib/rom/sql/migration/writer.rb +12 -4
  66. data/lib/rom/sql/order_dsl.rb +2 -0
  67. data/lib/rom/sql/plugin/associates.rb +4 -2
  68. data/lib/rom/sql/plugin/nullify.rb +37 -0
  69. data/lib/rom/sql/plugin/pagination.rb +2 -0
  70. data/lib/rom/sql/plugins.rb +4 -0
  71. data/lib/rom/sql/projection_dsl.rb +3 -1
  72. data/lib/rom/sql/rake_task.rb +2 -0
  73. data/lib/rom/sql/relation.rb +3 -1
  74. data/lib/rom/sql/relation/reading.rb +35 -7
  75. data/lib/rom/sql/relation/writing.rb +2 -0
  76. data/lib/rom/sql/restriction_dsl.rb +9 -1
  77. data/lib/rom/sql/schema.rb +16 -2
  78. data/lib/rom/sql/schema/attributes_inferrer.rb +5 -3
  79. data/lib/rom/sql/schema/dsl.rb +3 -1
  80. data/lib/rom/sql/schema/index_dsl.rb +5 -2
  81. data/lib/rom/sql/schema/inferrer.rb +12 -8
  82. data/lib/rom/sql/schema/type_builder.rb +4 -2
  83. data/lib/rom/sql/spec/support.rb +5 -3
  84. data/lib/rom/sql/tasks/migration_tasks.rake +16 -11
  85. data/lib/rom/sql/transaction.rb +2 -0
  86. data/lib/rom/sql/type_dsl.rb +2 -0
  87. data/lib/rom/sql/type_extensions.rb +4 -4
  88. data/lib/rom/sql/type_serializer.rb +2 -0
  89. data/lib/rom/sql/types.rb +2 -0
  90. data/lib/rom/sql/version.rb +3 -1
  91. data/lib/rom/sql/wrap.rb +2 -0
  92. data/lib/rom/types/values.rb +2 -0
  93. metadata +34 -32
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/dsl'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations'
2
4
 
3
5
  module ROM
@@ -48,7 +50,7 @@ module ROM
48
50
  # @see ROM::Command::ClassInterface.build
49
51
  #
50
52
  # @api public
51
- def build(relation, options = EMPTY_HASH)
53
+ def build(relation, **options)
52
54
  command = super
53
55
 
54
56
  configured_assocs = command.configured_associations
@@ -146,7 +148,7 @@ module ROM
146
148
  def with_association(name, opts = EMPTY_HASH)
147
149
  self.class.build(
148
150
  relation,
149
- { **options, associations: associations.merge(name => opts) }
151
+ **options, associations: associations.merge(name => opts)
150
152
  )
151
153
  end
152
154
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module SQL
5
+ module Plugin
6
+ # Nullify relation by
7
+ #
8
+ # @api public
9
+ module Nullify
10
+ if defined? JRUBY_VERSION
11
+ # Returns a relation that will never issue a query to the database. It
12
+ # implements the null object pattern for relations.
13
+ # Dataset#nullify doesn't work on JRuby, hence we fall back to SQL
14
+ #
15
+ # @api public
16
+ def nullify
17
+ where { `1 = 0` }
18
+ end
19
+ else
20
+ # Returns a relation that will never issue a query to the database. It
21
+ # implements the null object pattern for relations.
22
+ #
23
+ # @see http://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/null_dataset_rb.html
24
+ # @example result will always be empty, regardless if records exists
25
+ # users.where(name: 'Alice').nullify
26
+ #
27
+ # @return [SQL::Relation]
28
+ #
29
+ # @api public
30
+ def nullify
31
+ new(dataset.where { `1 = 0` }.__send__(__method__))
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/initializer'
2
4
 
3
5
  module ROM
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/plugins/relation/sql/instrumentation'
2
4
  require 'rom/plugins/relation/sql/auto_restrictions'
3
5
 
4
6
  require 'rom/sql/plugin/associates'
7
+ require 'rom/sql/plugin/nullify'
5
8
  require 'rom/sql/plugin/pagination'
6
9
 
7
10
  ROM.plugins do
8
11
  adapter :sql do
12
+ register :nullify, ROM::SQL::Plugin::Nullify, type: :relation
9
13
  register :pagination, ROM::SQL::Plugin::Pagination, type: :relation
10
14
  register :associates, ROM::SQL::Plugin::Associates, type: :command
11
15
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/dsl'
2
4
  require 'rom/sql/function'
3
5
 
@@ -29,7 +31,7 @@ module ROM
29
31
  # users.select { function(:count, :id).as(:total) }
30
32
  #
31
33
  # @param [Symbol] name SQL function
32
- # @param [Symbol] attr
34
+ # @param [Symbol] attrs
33
35
  #
34
36
  # @return [Rom::SQL::Function]
35
37
  #
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake'
2
4
  load 'rom/sql/tasks/migration_tasks.rake'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/types'
2
4
  require 'rom/sql/schema'
3
5
  require 'rom/sql/attribute'
@@ -35,7 +37,7 @@ module ROM
35
37
  table = opts[:from].first
36
38
 
37
39
  if db.table_exists?(table)
38
- select(*schema.map(&:qualified)).order(*schema.project(*schema.primary_key_names).qualified)
40
+ select(*schema.qualified_projection).order(*schema.project(*schema.primary_key_names).qualified)
39
41
  else
40
42
  self
41
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/support/inflector'
2
4
  require 'rom/sql/join_dsl'
3
5
 
@@ -807,7 +809,16 @@ module ROM
807
809
  #
808
810
  # @api public
809
811
  def union(relation, options = EMPTY_HASH, &block)
810
- new(dataset.__send__(__method__, relation.dataset, options, &block))
812
+ # We use the original relation name here if both relations have the
813
+ # same name. This makes it so if the user at some point references
814
+ # the relation directly by name later on things won't break in
815
+ # confusing ways.
816
+ same_relation = name == relation.name
817
+ alias_name = same_relation ? name : "#{name.to_sym}__#{relation.name.to_sym}"
818
+ opts = { alias: alias_name.to_sym, **options }
819
+
820
+ new_schema = schema.qualified(opts[:alias])
821
+ new_schema.(new(dataset.__send__(__method__, relation.dataset, opts, &block)))
811
822
  end
812
823
 
813
824
  # Checks whether a relation has at least one tuple
@@ -882,8 +893,8 @@ module ROM
882
893
  # @yieldparam relation [Array]
883
894
  #
884
895
  # @api public
885
- def lock(options = EMPTY_HASH, &block)
886
- clause = lock_clause(options)
896
+ def lock(**options, &block)
897
+ clause = lock_clause(**options)
887
898
 
888
899
  if block
889
900
  transaction do
@@ -986,7 +997,7 @@ module ROM
986
997
  # @return [SQL::Attribute]
987
998
  def query
988
999
  attr = schema.to_a[0]
989
- subquery = schema.project(attr).(self).dataset.unordered
1000
+ subquery = schema.project(attr).(self).dataset
990
1001
  SQL::Attribute[attr.type].meta(sql_expr: subquery)
991
1002
  end
992
1003
 
@@ -1002,6 +1013,21 @@ module ROM
1002
1013
  new(dataset.__send__(__method__))
1003
1014
  end
1004
1015
 
1016
+ # Wrap other relations using association names
1017
+ #
1018
+ # @example
1019
+ # tasks.wrap(:owner)
1020
+ #
1021
+ # @param [Array<Symbol>] names A list with association identifiers
1022
+ #
1023
+ # @return [Wrap]
1024
+ #
1025
+ # @api public
1026
+ def wrap(*names)
1027
+ others = names.map { |name| associations[name].wrapped }
1028
+ wrap_around(*others)
1029
+ end
1030
+
1005
1031
  private
1006
1032
 
1007
1033
  # Build a locking clause
@@ -1012,7 +1038,7 @@ module ROM
1012
1038
  stmt << ' OF ' << Array(of).join(', ') if of
1013
1039
 
1014
1040
  if skip_locked
1015
- raise ArgumentError, "SKIP LOCKED cannot be used with (NO)WAIT clause" if !wait.nil?
1041
+ raise ArgumentError, 'SKIP LOCKED cannot be used with (NO)WAIT clause' if !wait.nil?
1016
1042
 
1017
1043
  stmt << ' SKIP LOCKED'
1018
1044
  else
@@ -1048,9 +1074,11 @@ module ROM
1048
1074
  # @api private
1049
1075
  def __join__(type, other, join_cond = EMPTY_HASH, opts = EMPTY_HASH, &block)
1050
1076
  if other.is_a?(Symbol) || other.is_a?(ROM::Relation::Name)
1051
- if join_cond.empty?
1077
+ if join_cond.equal?(EMPTY_HASH) && !block
1052
1078
  assoc = associations[other]
1053
1079
  assoc.join(type, self)
1080
+ elsif block
1081
+ __join__(type, other, JoinDSL.new(schema).(&block), opts)
1054
1082
  else
1055
1083
  new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
1056
1084
  end
@@ -1066,7 +1094,7 @@ module ROM
1066
1094
  join_opts = EMPTY_HASH
1067
1095
  end
1068
1096
 
1069
- new(dataset.__send__(type, other.name.to_sym, join_cond, join_opts))
1097
+ new(dataset.__send__(type, other.name.dataset.to_sym, join_cond, join_opts))
1070
1098
  else
1071
1099
  associations[other.name.key].join(type, self, other)
1072
1100
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  class Relation < ROM::Relation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/dsl'
2
4
 
3
5
  module ROM
@@ -6,7 +8,13 @@ module ROM
6
8
  class RestrictionDSL < DSL
7
9
  # @api private
8
10
  def call(&block)
9
- instance_exec(select_relations(block.parameters), &block)
11
+ arg, kwargs = select_relations(block.parameters)
12
+
13
+ if kwargs.nil?
14
+ instance_exec(arg, &block)
15
+ else
16
+ instance_exec(**kwargs, &block)
17
+ end
10
18
  end
11
19
 
12
20
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/schema'
2
4
 
3
5
  require 'rom/sql/schema/dsl'
@@ -65,6 +67,18 @@ module ROM
65
67
  new(map { |attr| attr.qualified(table_alias) })
66
68
  end
67
69
 
70
+ # Return a new schema with attributes that are aliased
71
+ # and marked as qualified
72
+ #
73
+ # Intended to be used when passing attributes to `dataset#select`
74
+ #
75
+ # @return [Schema]
76
+ #
77
+ # @api public
78
+ def qualified_projection(table_alias = nil)
79
+ new(map { |attr| attr.qualified_projection(table_alias) })
80
+ end
81
+
68
82
  # Project a schema
69
83
  #
70
84
  # @see ROM::Schema#project
@@ -127,7 +141,7 @@ module ROM
127
141
  #
128
142
  # @api public
129
143
  def call(relation)
130
- relation.new(relation.dataset.select(*self), schema: self)
144
+ relation.new(relation.dataset.select(*self.qualified_projection), schema: self)
131
145
  end
132
146
 
133
147
  # Return an empty schema
@@ -142,7 +156,7 @@ module ROM
142
156
  # Finalize all attributes by qualifying them and initializing primary key names
143
157
  #
144
158
  # @api private
145
- def finalize_attributes!(options = EMPTY_HASH)
159
+ def finalize_attributes!(**options)
146
160
  super do
147
161
  @attributes = map(&:qualified)
148
162
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/class_attributes'
2
4
 
3
5
  module ROM
@@ -22,8 +24,8 @@ module ROM
22
24
 
23
25
  columns = filter_columns(gateway.connection.schema(dataset))
24
26
 
25
- inferred = columns.map do |(name, definition)|
26
- type = type_builder.(definition)
27
+ inferred = columns.map do |name, definition|
28
+ type = type_builder.(**definition)
27
29
 
28
30
  attr_class.new(type.meta(source: schema.name), name: name) if type
29
31
  end.compact
@@ -42,7 +44,7 @@ module ROM
42
44
 
43
45
  # @api private
44
46
  def filter_columns(schema)
45
- schema.reject { |(_, definition)| definition[:db_type] == CONSTRAINT_DB_TYPE }
47
+ schema.reject { |_, definition| definition[:db_type] == CONSTRAINT_DB_TYPE }
46
48
  end
47
49
  end
48
50
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/schema/index_dsl'
2
4
 
3
5
  module ROM
@@ -15,7 +17,7 @@ module ROM
15
17
  #
16
18
  # @api public
17
19
  def indexes(&block)
18
- @index_dsl = IndexDSL.new(options, &block)
20
+ @index_dsl = IndexDSL.new(**options, &block)
19
21
  end
20
22
 
21
23
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module ROM
@@ -19,6 +21,7 @@ module ROM
19
21
 
20
22
  instance_exec(&block)
21
23
  end
24
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
22
25
 
23
26
  # @api public
24
27
  def index(*attributes, **options)
@@ -28,7 +31,7 @@ module ROM
28
31
  # @api private
29
32
  def call(schema_name, attrs)
30
33
  attributes = attrs.map do |attr|
31
- attr_class.new(attr[:type], attr[:options] || {}).meta(source: schema_name)
34
+ attr_class.new(attr[:type], **(attr[:options] || {})).meta(source: schema_name)
32
35
  end
33
36
 
34
37
  registry.map { |attr_names, options|
@@ -44,7 +47,7 @@ module ROM
44
47
  attributes.find { |a| a.name == name }.unwrap
45
48
  end
46
49
 
47
- Index.new(index_attributes, options)
50
+ Index.new(index_attributes, **options)
48
51
  end
49
52
  end
50
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  require 'rom/sql/schema/type_builder'
@@ -32,9 +34,9 @@ module ROM
32
34
  # @api private
33
35
  def call(schema, gateway)
34
36
  if enabled?
35
- infer_from_database(gateway, schema, super)
37
+ infer_from_database(gateway, schema, **super)
36
38
  else
37
- infer_from_attributes(gateway, schema, super)
39
+ infer_from_attributes(gateway, schema, **super)
38
40
  end
39
41
  rescue Sequel::Error => error
40
42
  on_error(schema.name, error)
@@ -54,7 +56,7 @@ module ROM
54
56
  end
55
57
 
56
58
  # @api private
57
- def infer_from_attributes(gateway, schema, attributes:, **rest)
59
+ def infer_from_attributes(_gateway, schema, attributes:, **rest)
58
60
  indexes = schema.indexes | indexes_from_attributes(attributes)
59
61
  foreign_keys = foreign_keys_from_attributes(attributes)
60
62
 
@@ -69,7 +71,8 @@ module ROM
69
71
  if gateway.connection.respond_to?(:indexes)
70
72
  dataset = schema.name.dataset
71
73
 
72
- gateway.connection.indexes(dataset).map { |index_name, columns:, unique:, **rest|
74
+ gateway.connection.indexes(dataset).map { |index_name, definition|
75
+ columns, unique = definition.values_at(:columns, :unique)
73
76
  attrs = columns.map { |name| attributes[name] }
74
77
 
75
78
  SQL::Index.new(attrs, name: index_name, unique: unique)
@@ -83,7 +86,8 @@ module ROM
83
86
  def foreign_keys_from_database(gateway, schema, attributes)
84
87
  dataset = schema.name.dataset
85
88
 
86
- gateway.connection.foreign_key_list(dataset).map { |columns:, table:, key:, **rest|
89
+ gateway.connection.foreign_key_list(dataset).map { |definition|
90
+ columns, table, key = definition.values_at(:columns, :table, :key)
87
91
  attrs = columns.map { |name| attributes[name] }
88
92
 
89
93
  SQL::ForeignKey.new(attrs, table, parent_keys: key)
@@ -147,9 +151,9 @@ module ROM
147
151
  raise e
148
152
  elsif !silent
149
153
  warn "[#{dataset}] failed to infer schema. " \
150
- "Make sure tables exist before ROM container is set up. " \
151
- "This may also happen when your migration tasks load ROM container, " \
152
- "which is not needed for migrations as only the connection is required " \
154
+ 'Make sure tables exist before ROM container is set up. ' \
155
+ 'This may also happen when your migration tasks load ROM container, ' \
156
+ 'which is not needed for migrations as only the connection is required ' \
153
157
  "(#{e.message})"
154
158
  end
155
159
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  class Schema
@@ -37,9 +39,9 @@ module ROM
37
39
 
38
40
  def call(primary_key:, db_type:, type:, allow_null:, **rest)
39
41
  if primary_key
40
- map_pk_type(type, db_type, rest)
42
+ map_pk_type(type, db_type, **rest)
41
43
  else
42
- mapped_type = map_type(type, db_type, rest)
44
+ mapped_type = map_type(type, db_type, **rest)
43
45
 
44
46
  if mapped_type
45
47
  read_type = mapped_type.meta[:read]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if defined? JRUBY_VERSION
2
4
  USING_JRUBY = true
3
5
  else
@@ -5,15 +7,15 @@ else
5
7
  end
6
8
 
7
9
  if USING_JRUBY
8
- SEQUEL_TEST_DB_URI = "jdbc:sqlite::memory:"
10
+ SEQUEL_TEST_DB_URI = 'jdbc:sqlite::memory:'
9
11
  else
10
- SEQUEL_TEST_DB_URI = "sqlite::memory"
12
+ SEQUEL_TEST_DB_URI = 'sqlite::memory'
11
13
  end
12
14
 
13
15
  DB = Sequel.connect(SEQUEL_TEST_DB_URI)
14
16
 
15
17
  def seed(db = DB)
16
- db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name STRING)")
18
+ db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name STRING)')
17
19
 
18
20
  db[:users].insert(id: 1, name: 'Jane')
19
21
  db[:users].insert(id: 2, name: 'Joe')