pg_conn 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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