pg_conn 0.15.0 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca254f7c17749713f46f07448221683e5975c570407aff61ee440f82a37466ca
4
- data.tar.gz: 86486e52477bbf36009d69f9322061077b5860582e4fc7ced67e2d88bad5ee0b
3
+ metadata.gz: 25c23025d55818536e6d89bc5fa40b12ac296f0e61657db387f8a8732bdf3504
4
+ data.tar.gz: 7592d3a2b26d7f75f0ea88ad9b8f951422ced0cc913a387eb3f774b31bb1c4eb
5
5
  SHA512:
6
- metadata.gz: b0981561372e9039eda5c8eb85948c63559e1875f4764dfaa3b32d52fa501b62522b2acde9a58036aa6978bb9b454cb66bec788e9d141dbc50b3ce5e14ee9941
7
- data.tar.gz: 70a3927ceec9d61962002e301c817479e043878ec122ed1d265c9ed9372fb0abc744eaaf266e983b6352179f42e983752c030a12e878699c24b37fde8c6c663e
6
+ metadata.gz: 0701ef07551bc1db06b654aa12e957ca4ad2286f1050c122af95ce78670ea426e6bf8f566831070200588e2c844eaf70ddab0cc5147941ccdcf6f4dc232c4102
7
+ data.tar.gz: 5310090682a9c93a648823b477081eebe31c214554ebe06a44d2f6702ec18c3fd5eb43e16bad0c15178d48a6194e109dbf15adb8bb3f75e645b90581ef3bc948
@@ -69,8 +69,8 @@ module PgConn
69
69
  end
70
70
 
71
71
  # Hollow-out a database by removing all schemas in the database. The public
72
- # schema is recreated afterwards unless if :public is false. Uses the
73
- # current database if @database is nil
72
+ # schema is recreated afterwards unless :public is false. Uses the current
73
+ # database if @database is nil
74
74
  #
75
75
  # Note that the database can have active users logged in while the database
76
76
  # is emptied. TODO Explain what happens if the users have active
@@ -103,7 +103,7 @@ module PgConn
103
103
  create(to_database, owner: owner, template: from_database)
104
104
  end
105
105
 
106
- # TODO: This code is replicated across many project. Should be moved to PgConn
106
+ # TODO: This code is replicated across many projects. Should be moved to PgConn
107
107
  def load(database, file, role: ENV['USER'], gzip: nil)
108
108
  command_opt = role ? "-c \"set role #{role}\";\n" : nil
109
109
  if gzip
@@ -118,7 +118,7 @@ module PgConn
118
118
  status == 0 or raise PsqlError.new(stderr)
119
119
  end
120
120
 
121
- # TODO: This code is replicated across many project. Should be moved to PgConn
121
+ # TODO: This code is replicated across many projects. Should be moved to PgConn
122
122
  def save(database, file, data: true, schema: true, gzip: nil)
123
123
  data_opt = data ? nil : "--schema-only"
124
124
  schema_opt = schema ? nil : "--data-only"
@@ -35,6 +35,26 @@ module PgConn
35
35
  true
36
36
  end
37
37
 
38
+ # Hollow out a schema by dropping all tables and views (but still not
39
+ # functions and procedures TODO)
40
+ def empty!(schema, exclude: [])
41
+ self.list_tables(schema, exclude: exclude).each { |table|
42
+ conn.exec "drop table if exists #{schema}.#{table} cascade"
43
+ }
44
+ self.list_views(schema, exclude: exclude).each { |view|
45
+ conn.exec "drop view if exists #{schema}.#{view} cascade"
46
+ }
47
+ end
48
+
49
+ # Empty all tables in the given schema
50
+ def clean!(schema, exclude: [])
51
+ conn.session.triggers(false) {
52
+ self.list_tables(schema, exclude: exclude).each { |table|
53
+ conn.exec "delete from #{schema}.#{table}"
54
+ }
55
+ }
56
+ end
57
+
38
58
  # List schemas. Built-in schemas are not listed unless the :all option is
39
59
  # true. The :exclude option can be used to exclude named schemas
40
60
  def list(all: false, exclude: [])
@@ -53,7 +73,7 @@ module PgConn
53
73
 
54
74
  # Return true if table exists
55
75
  def exist_table?(schema, table)
56
- conn.exist?(relation_exist_query(schema, table, kind: %w(r f)))
76
+ conn.exist? relation_exist_query(schema, table, kind: %w(r f))
57
77
  end
58
78
 
59
79
  # Return true if view exists
@@ -66,19 +86,22 @@ module PgConn
66
86
  conn.exist? column_exist_query(schema, relation, column)
67
87
  end
68
88
 
89
+ # TODO
90
+ # def exist_index?(schema, relation, FIXME)
91
+
69
92
  # Return list of relations in the schema
70
- def list_relations(schema)
71
- conn.values relation_list_query(schema)
93
+ def list_relations(schema, exclude: [])
94
+ conn.values relation_list_query(schema, exclude: exclude)
72
95
  end
73
96
 
74
97
  # Return list of tables in the schema
75
- def list_tables(schema)
76
- conn.values relation_list_query(schema, kind: %w(r f))
98
+ def list_tables(schema, exclude: [])
99
+ conn.values relation_list_query(schema, exclude: exclude, kind: %w(r f))
77
100
  end
78
101
 
79
102
  # Return list of view in the schema
80
- def list_views(schema)
81
- conn.values relation_list_query(schema, kind: %w(v m))
103
+ def list_views(schema, exclude: [])
104
+ conn.values relation_list_query(schema, exclude: exclude, kind: %w(v m))
82
105
  end
83
106
 
84
107
  # Return a list of columns. If +relation+ is defined, only columns from that
@@ -113,13 +136,18 @@ module PgConn
113
136
  )
114
137
  end
115
138
 
116
- def relation_list_query(schema, kind: nil)
117
- kind_sql_list = "'" + (kind.nil? ? %w(r f v m) : Array(kind).flatten).join("', '") + "'"
139
+ def relation_list_query(schema, exclude: nil, kind: nil)
140
+ kind_list = "'" + (kind.nil? ? %w(r f v m) : Array(kind).flatten).join("', '") + "'"
141
+ kind_expr = "relkind in (#{kind_list})"
142
+ exclude = Array(exclude || []).flatten
143
+ exclude_list = "'#{exclude.flatten.join("', '")}'" if !exclude.empty?
144
+ exclude_expr = exclude.empty? ? "true = true" : "not relname in (#{exclude_list})"
118
145
  %(
119
146
  select relname
120
147
  from pg_class
121
148
  where relnamespace::regnamespace::text = '#{schema}'
122
- and relkind in (#{kind_sql_list})
149
+ and #{kind_expr}
150
+ and #{exclude_expr}
123
151
  )
124
152
  end
125
153
 
@@ -8,7 +8,8 @@ module PgConn
8
8
  end
9
9
 
10
10
  # Returns a list of users connected to the given database. If database is
11
- # nil, it returns a list of database/username tuples for all connected users
11
+ # nil, it returns a list of database/username tuples for all connected
12
+ # users
12
13
  def list(database)
13
14
  if database
14
15
  conn.values "select usename from pg_stat_activity where datname = '#{database}'"
@@ -21,34 +22,52 @@ module PgConn
21
22
  end
22
23
  end
23
24
 
24
- # Terminate sessions in the database of the given users or of all users if
25
- # the users is nil. Note that 'terminate(database)' is a nop because the
26
- # absent users argument defaults to an empty list
27
- #
28
- # TODO: Make is possible to terminate a single session of a user with
29
- # multiple sessions (is this ever relevant?)
30
- def terminate(database, *users)
25
+ # Return true if the given database accepts connections
26
+ def enabled?(database)
31
27
  !database.nil? or raise ArgumentError
32
- users = Array(users).flatten
33
- case users
34
- when []; return
35
- when [nil]; users = list(database)
36
- else users = Array(users).flatten
37
- end
38
- pids = self.pids(database, users)
39
- return if pids.empty?
40
- pids_sql = pids.map { |pid| "(#{pid})" }.join(", ")
41
- conn.execute "select pg_terminate_backend(pid) from ( values #{pids_sql} ) as x(pid)"
28
+ conn.value "select datallowconn from pg_catalog.pg_database where datname = '#{database}'"
42
29
  end
43
30
 
31
+ # Ensure connections to the given database are enabled
32
+ def enable(database)
33
+ !database.nil? or raise ArgumentError
34
+ conn.execute "alter database #{database} allow_connections = true"
35
+ end
36
+
37
+ # Ensure connections to the given database are disabled
44
38
  def disable(database)
45
39
  !database.nil? or raise ArgumentError
46
40
  conn.execute "alter database #{database} allow_connections = false"
47
41
  end
48
42
 
49
- def enable(database)
43
+ # TODO: Why not let a nil database argument have the current database as default?
44
+
45
+ # Terminate sessions in the database of the given users or of all users if
46
+ # nil. Note that 'terminate(database)' is a nop because the absent users
47
+ # argument defaults to an empty list
48
+ #
49
+ # TODO: Make is possible to terminate a single session of a user with
50
+ # multiple sessions (is this ever relevant?)
51
+ #
52
+ def terminate(database, *users)
50
53
  !database.nil? or raise ArgumentError
51
- conn.execute "alter database #{database} allow_connections = true"
54
+ enabled = self.enabled?(database)
55
+
56
+ case users
57
+ when [];
58
+ return
59
+ when [nil]
60
+ self.disable(database) if enabled
61
+ users = self.list(database)
62
+ else
63
+ users = Array(users).flatten
64
+ end
65
+ pids = self.pids(database, users)
66
+ if !pids.empty?
67
+ pids_sql = pids.map { |pid| "(#{pid})" }.join(", ")
68
+ conn.execute "select pg_terminate_backend(pid) from ( values #{pids_sql} ) as x(pid)"
69
+ end
70
+ self.enable(database) if self.enabled?(database) != enabled
52
71
  end
53
72
 
54
73
  # Run block without any connected users. Existing sessions are terminated
@@ -56,14 +75,46 @@ module PgConn
56
75
  !database.nil? or raise ArgumentError
57
76
  begin
58
77
  disable(database)
59
- users = list(database)
60
- terminate(database, users)
78
+ terminate(database, nil)
61
79
  yield
62
80
  ensure
63
81
  enable(database)
64
82
  end
65
83
  end
66
84
 
85
+ # Return true if session triggers are enabled. Triggers are enabled by
86
+ # default by Postgres
87
+ def triggers?() conn.value "select current_setting('session_replication_role') = 'replica'" end
88
+
89
+ # Enable session triggers
90
+ def enable_triggers()
91
+ conn.execute "set session session_replication_role = replica"
92
+ end
93
+
94
+ # Disable session triggers
95
+ def disable_triggers()
96
+ conn.execute "set session session_replication_role = DEFAULT"
97
+ end
98
+
99
+ # Execute block with session triggers on or off
100
+ def triggers(on_off, &block)
101
+ begin
102
+ active = triggers?
103
+ if on_off && !active
104
+ enable_triggers
105
+ elsif !on_off && active
106
+ disable_triggers
107
+ end
108
+ yield
109
+ ensure
110
+ if on_off && !active
111
+ disable_triggers
112
+ elsif !on_off && active
113
+ enable_triggers
114
+ end
115
+ end
116
+ end
117
+
67
118
  private
68
119
  # Like #list but returns the PIDs of the users
69
120
  def pids(database, users)
@@ -1,3 +1,3 @@
1
1
  module PgConn
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
data/lib/pg_conn.rb CHANGED
@@ -67,9 +67,15 @@ module PgConn
67
67
  # #exec or #transaction block
68
68
  attr_reader :timestamp
69
69
 
70
- # PG::Error object if the last statement failed; otherwise nil
70
+ # PG::Error object of the first failed statement in the transaction;
71
+ # otherwise nil. It is cleared at the beginning of a transaction so be sure
72
+ # to save it before you run any cleanup code that may initiate new
73
+ # transactions
71
74
  attr_reader :error
72
75
 
76
+ # True if the transaction is in a error state
77
+ def error?() !@error.nil? end
78
+
73
79
  # Tuple of error message, lineno, and charno of the error object where each
74
80
  # element defaults to nil if not found
75
81
  def err
@@ -607,24 +613,17 @@ module PgConn
607
613
  #
608
614
  # TODO: Make sure the transaction stack is emptied on postgres errors
609
615
  def exec(sql, commit: true, fail: true, silent: false)
610
- begin
611
- transaction(commit: commit) { execute(sql, fail: fail, silent: silent) }
612
- rescue PG::Error
613
- raise if fail
614
- cancel_transaction
615
- return nil
616
- end
616
+ transaction(commit: commit) { execute(sql, fail: fail, silent: silent) }
617
617
  end
618
618
 
619
619
  # Like #exec but returns true/false depending on if the command succeeded.
620
- # There is not corresponding #exeucte? method because any failure rolls
620
+ # There is not a corresponding #execute? method because any failure rolls
621
621
  # back the whole transaction stack. TODO: Check which exceptions that
622
622
  # should be captured
623
623
  def exec?(sql, commit: true, silent: true)
624
624
  begin
625
625
  exec(sql, commit: commit, fail: true, silent: silent)
626
626
  rescue PG::Error
627
- cancel_transaction
628
627
  return false
629
628
  end
630
629
  return true
@@ -643,6 +642,7 @@ module PgConn
643
642
  begin
644
643
  pg_exec(sql, silent: silent)&.cmd_tuples
645
644
  rescue PG::Error
645
+ cancel_transaction
646
646
  raise if fail
647
647
  return nil
648
648
  end
@@ -672,7 +672,7 @@ module PgConn
672
672
 
673
673
  def commit()
674
674
  if transaction?
675
- pop_transaction
675
+ pop_transaction(fail: false)
676
676
  else
677
677
  pg_exec("commit")
678
678
  end
@@ -680,11 +680,18 @@ module PgConn
680
680
 
681
681
  def rollback() raise Rollback end
682
682
 
683
- # True if a transaction is in progress. Note that this requires all
684
- # transactions to be started using PgConn's transaction methods;
685
- # transactions started using raw SQL are not registered
683
+ # True if a transaction is in progress
684
+ #
685
+ # Note that this requires all transactions to be started using PgConn's
686
+ # transaction methods; transactions started using raw SQL are not
687
+ # registered
686
688
  def transaction?() !@savepoints.nil? end
687
689
 
690
+ # True if a database transaction is in progress
691
+ def database_transaction?
692
+ pg_exec("select transaction_timestamp() != statement_timestamp()", fail: false)
693
+ end
694
+
688
695
  # Returns number of transaction or savepoint levels
689
696
  def transactions() @savepoints ? 1 + @savepoints.size : 0 end
690
697
 
@@ -696,11 +703,12 @@ module PgConn
696
703
  else
697
704
  @savepoints = []
698
705
  pg_exec("begin")
706
+ @error = @err = nil
699
707
  @timestamp = pg_exec("select current_timestamp").values[0][0] if @pg_connection
700
708
  end
701
709
  end
702
710
 
703
- def pop_transaction(commit: true, fail: true)
711
+ def pop_transaction(commit: true, fail: true, exception: true)
704
712
  if transaction?
705
713
  if savepoint = @savepoints.pop
706
714
  if !commit
@@ -728,8 +736,10 @@ module PgConn
728
736
  begin
729
737
  pg_exec("rollback")
730
738
  rescue PG::Error
739
+ ;
731
740
  end
732
741
  @savepoints = nil
742
+ true
733
743
  end
734
744
 
735
745
  # Start a transaction. If called with a block, the block is executed within
@@ -748,13 +758,14 @@ module PgConn
748
758
  push_transaction
749
759
  result = yield
750
760
  rescue PgConn::Rollback
751
- pop_transaction(commit: false)
761
+ pop_transaction(commit: false, fail: false)
752
762
  return nil
753
763
  rescue PG::Error
764
+ cancel_transaction
754
765
  @savepoints = nil
755
766
  raise
756
767
  end
757
- pop_transaction(commit: commit)
768
+ pop_transaction(commit: commit, fail: false)
758
769
  result
759
770
  else
760
771
  push_transaction
@@ -812,7 +823,6 @@ module PgConn
812
823
  # TODO: Fix silent by not handling exceptions
813
824
  def pg_exec(arg, silent: false)
814
825
  if @pg_connection
815
- @error = @err = nil
816
826
  begin
817
827
  last_stmt = nil # To make the current SQL statement visible to the rescue clause. FIXME Not used?
818
828
  if arg.is_a?(String)
@@ -826,9 +836,11 @@ module PgConn
826
836
  last_stmt = stmts.last
827
837
  @pg_connection.exec(stmts.join(";\n"))
828
838
  end
829
-
830
839
  rescue PG::Error => ex
831
- @error = ex
840
+ if @error.nil?
841
+ @error = ex
842
+ @err = nil
843
+ end
832
844
  if !silent # FIXME Why do we handle this?
833
845
  $stderr.puts arg
834
846
  $stderr.puts
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_conn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-11 00:00:00.000000000 Z
11
+ date: 2024-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg