pg_conn 0.2.1
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 +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/README.md +35 -0
- data/Rakefile +8 -0
- data/TODO +70 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/pg_conn/rdbms_methods.rb +136 -0
- data/lib/pg_conn/role_methods.rb +96 -0
- data/lib/pg_conn/schema_methods.rb +157 -0
- data/lib/pg_conn/version.rb +3 -0
- data/lib/pg_conn.rb +677 -0
- data/pg_conn.gemspec +54 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df5aa73c1d8e34263b51b29acb63f2d675e7945141e44d9f5adbd00987d1bb46
|
4
|
+
data.tar.gz: 56ad22f1c45fb8e78d43f59ff4b982da09d86d4e1fdfd802cf7e396d66a16ece
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b94e9e6b1ed7605aceab03fb0393e79f5f57ef1e7bb26794cd8b25a8de93c7f4358686dc910a2d76132941daa7c2ce113d20c45144ff9e2ff6fb3e5a744f96b6
|
7
|
+
data.tar.gz: 386d086a0af25b2f049046a083e1af1f947a7fbed63b0a5787659fe95414678761d93fcddb4ddce04b7a97d1dcb82160c7ff6bf79ec4aabe1d18ec49f55d62b9
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.7.1
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# PgConn
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pg_conn`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'pg_conn'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install pg_conn
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pg_conn.
|
data/Rakefile
ADDED
data/TODO
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
TODO
|
2
|
+
o fix silent
|
3
|
+
o fix fail
|
4
|
+
o A PgConn.new with a block. Useful for temporary database connections (a
|
5
|
+
rather specialized use-case, though)
|
6
|
+
o A "#bag" method that allows duplicate IDs
|
7
|
+
o Generalize #set using :element_type option. This makes #table a special case of #set
|
8
|
+
o Maybe grant/revoke methods. Problem that there is large number of variations
|
9
|
+
# :callseq:
|
10
|
+
# grant(role, to_role)
|
11
|
+
# grant(privilege, subject, to_role)
|
12
|
+
def grant(role, to_role)
|
13
|
+
#
|
14
|
+
conn.exec "grant #{role} to #{to_role}"
|
15
|
+
end
|
16
|
+
o Allow a :type argument to all query methods that can be used to specify the
|
17
|
+
composition of anonymous record types
|
18
|
+
|
19
|
+
|
20
|
+
REFACTOR
|
21
|
+
#!/usr/bin/env ruby
|
22
|
+
|
23
|
+
module PgConn
|
24
|
+
# Global PgConn instance
|
25
|
+
def self.instance() @@pg_conn_instance ||= PgConn.new end
|
26
|
+
def self.instance=(pg_conn) @@pg_conn_instance = pg_conn end
|
27
|
+
|
28
|
+
def self.connect(*args)
|
29
|
+
conn = PgConn.new(*args)
|
30
|
+
@@pg_conn_instance ||= conn
|
31
|
+
conn
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.new() raise "TODO" end
|
35
|
+
|
36
|
+
class PgConn
|
37
|
+
include PgQuery
|
38
|
+
include PgTransaction
|
39
|
+
attr_reader :schema
|
40
|
+
attr_reader :role
|
41
|
+
attr_reader :rdbms
|
42
|
+
end
|
43
|
+
|
44
|
+
class Connection
|
45
|
+
attr_reader :query
|
46
|
+
attr_reader :transaction
|
47
|
+
attr_reader :schema
|
48
|
+
attr_reader :role
|
49
|
+
attr_reader :rdbms
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module SchemaMethods
|
54
|
+
def conn() "schema_methods_conn" end
|
55
|
+
def schema_doit() puts "#{conn}: schema_doit" end
|
56
|
+
end
|
57
|
+
|
58
|
+
class SchemaMethodsObject
|
59
|
+
include SchemaMethods
|
60
|
+
def conn() "schema_methods_object_conn" end
|
61
|
+
def doit() schema_doit end
|
62
|
+
end
|
63
|
+
|
64
|
+
s = SchemaMethodsObject.new
|
65
|
+
s.doit
|
66
|
+
s.schema_doit
|
67
|
+
|
68
|
+
# Example
|
69
|
+
include PgConn
|
70
|
+
p values "select * from t" # Connection is set up by default
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "pg_conn"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
|
2
|
+
module PgConn
|
3
|
+
class Error < StandardError; end # Move to exception.rb
|
4
|
+
class PsqlError < Error; end
|
5
|
+
|
6
|
+
class RdbmsMethods
|
7
|
+
attr_reader :conn
|
8
|
+
|
9
|
+
def initialize(conn)
|
10
|
+
@conn = conn
|
11
|
+
# TODO: Check if conn is a superuser connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return true if the database exists
|
15
|
+
def exist?(database)
|
16
|
+
conn.exist? %(
|
17
|
+
select 1
|
18
|
+
from pg_database
|
19
|
+
where datname = '#{database}'
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a new database
|
24
|
+
def create(database, owner: ENV['USER'], template: "template1")
|
25
|
+
owner_clause = owner ? "owner = \"#{owner}\"" : nil
|
26
|
+
template_clause = template ? "template = \"#{template}\"" : nil
|
27
|
+
stmt = ["create database \"#{database}\"", owner_clause, template_clause].compact.join(" ")
|
28
|
+
conn.execute stmt # Note we're using #execute instead of #exec because
|
29
|
+
# create database can't run within a transaction
|
30
|
+
end
|
31
|
+
|
32
|
+
# Drop a database
|
33
|
+
def drop(database)
|
34
|
+
conn.execute "drop database if exists \"#{database}\""
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# List databases in the RDBMS
|
39
|
+
def list(all: false, exclude: [])
|
40
|
+
exclude += POSTGRES_DATABASES if !all
|
41
|
+
exclude_sql_list = "'" + exclude.join("', '") + "'"
|
42
|
+
exclude_clause = exclude.empty? ? nil : "where datname not in (#{exclude_sql_list})"
|
43
|
+
stmt = ["select datname from pg_database", exclude_clause].compact.join(" ")
|
44
|
+
conn.values stmt
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the owner of the database
|
48
|
+
def owner(database)
|
49
|
+
conn.value %(
|
50
|
+
select r.rolname
|
51
|
+
from (values ('#{database}')) as v (database)
|
52
|
+
left join pg_database d on d.datname = v.database
|
53
|
+
left join pg_roles r on r.oid = d.datdba
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return list of users currently logged in to the database or to any
|
58
|
+
# database if database is nil
|
59
|
+
#
|
60
|
+
# FIXME: There is a possible race-condition here where some process
|
61
|
+
# (auto-vacuum) is logged in to the database but has a nil username. The
|
62
|
+
# easy fix is to have usename not null but it would be nice to know what
|
63
|
+
# exactly is triggering this problem
|
64
|
+
#
|
65
|
+
def users(database)
|
66
|
+
database_clause = database ? "datname = '#{database}'" : nil
|
67
|
+
query = ["select usename from pg_stat_activity", database_clause].compact.join(" where ")
|
68
|
+
conn.values query
|
69
|
+
end
|
70
|
+
|
71
|
+
# Hollow-out a database by removing all schemas in the database. The public
|
72
|
+
# schema is recreated afterwards. Use the current database if @database is
|
73
|
+
# nil
|
74
|
+
#
|
75
|
+
# Note that the database can have active users logged in while the database
|
76
|
+
# is emptied
|
77
|
+
#
|
78
|
+
def empty!(database = nil, exclude: [])
|
79
|
+
local = !database.nil?
|
80
|
+
begin
|
81
|
+
conn = local ? PgConn.new(database) : self.conn
|
82
|
+
schemas =
|
83
|
+
conn
|
84
|
+
.values("select nspname from pg_namespace where nspowner != 10 or nspname = 'public'")
|
85
|
+
.select { |schema| !exclude.include?(schema) }
|
86
|
+
.join(", ")
|
87
|
+
conn.exec %(
|
88
|
+
drop schema #{schemas} cascade;
|
89
|
+
create schema public authorization postgres;
|
90
|
+
grant usage, create on schema public to public
|
91
|
+
)
|
92
|
+
ensure
|
93
|
+
conn&.terminate if local
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Fast copy using templates. Note that no user may be logged in to the
|
98
|
+
# source database for this to work
|
99
|
+
def copy(from_database, to_database, owner: ENV['USER'])
|
100
|
+
create(to_database, owner: owner, template: from_database)
|
101
|
+
end
|
102
|
+
|
103
|
+
def load(database, file, role: ENV['USER'], gzip: nil)
|
104
|
+
command_opt = role ? "-c \"set role #{role}\";\n" : nil
|
105
|
+
if gzip
|
106
|
+
pipe_cmd = file ? "gunzip --to-stdout #{file} |" : "gunzip --to-stdout |"
|
107
|
+
file_opt = nil
|
108
|
+
else
|
109
|
+
pipe_cmd = nil
|
110
|
+
file_opt = file ? "-f #{file}" : nil
|
111
|
+
end
|
112
|
+
cmd = [pipe_cmd, "psql -v ON_ERROR_STOP=1", command_opt, file_opt, database].compact.join(" ")
|
113
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
114
|
+
status == 0 or raise PsqlError.new(stderr)
|
115
|
+
end
|
116
|
+
|
117
|
+
def save(database, file, data: true, schema: true, gzip: nil)
|
118
|
+
data_opt = data ? nil : "--schema-only"
|
119
|
+
schema_opt = schema ? nil : "--data-only"
|
120
|
+
if gzip
|
121
|
+
pipe_cmd = file ? "| gzip >#{file}" : "| gzip"
|
122
|
+
file_opt = nil
|
123
|
+
else
|
124
|
+
pipe_cmd = nil
|
125
|
+
file_opt = file ? "-f #{file}" : nil
|
126
|
+
end
|
127
|
+
cmd = ["pg_dump --no-owner", data_opt, schema_opt, file_opt, database, pipe_cmd].compact.join(" ")
|
128
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
129
|
+
status == 0 or raise PsqlError.new(stderr)
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
POSTGRES_DATABASES = %w(template0 template1 postgres)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module PgConn
|
2
|
+
class RoleMethods
|
3
|
+
attr_reader :conn
|
4
|
+
|
5
|
+
def initialize(conn)
|
6
|
+
@conn = conn
|
7
|
+
# TODO: Check if conn is a superuser connection
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return true if role(s) exists
|
11
|
+
def exist?(*rolenames, superuser: nil, can_login: nil)
|
12
|
+
rolenames = Array(rolenames).flatten.compact
|
13
|
+
rolename_clause = "rolname in (#{PgConn.sql_values(rolenames)})"
|
14
|
+
superuser_clause =
|
15
|
+
case superuser
|
16
|
+
when true; "rolsuper"
|
17
|
+
when false; "not rolsuper"
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
can_login_clause =
|
22
|
+
case can_login
|
23
|
+
when true; "rolcanlogin"
|
24
|
+
when false; "not rolcanlogin"
|
25
|
+
else
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
where_clause = [rolename_clause, superuser_clause, can_login_clause].compact.join(" and ")
|
29
|
+
conn.value("select count(*) from pg_roles where #{where_clause}") == rolenames.size
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return true if the user can login
|
33
|
+
def can_login?(username, superuser: nil)
|
34
|
+
exist?(username, superuser: superuser, can_login: true)
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :user?, :can_login?
|
38
|
+
|
39
|
+
# Return true if the user is a superuser
|
40
|
+
def superuser?(username)
|
41
|
+
exist?(username, superuser: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a new role
|
45
|
+
def create(rolename, superuser: false, create_database: false, can_login: false, create_role: false)
|
46
|
+
user_decl = "create role \"#{rolename}\""
|
47
|
+
superuser_decl = superuser ? "--superuser" : nil
|
48
|
+
create_database_decl = create_database ? "--createdb" : "--nocreatedb"
|
49
|
+
can_login_decl = can_login ? "--login" : "--nologin"
|
50
|
+
create_role_decl = create_role ? "--createrole" : "--nocreaterole"
|
51
|
+
|
52
|
+
stmt = [user_decl, superuser_decl, can_login_decl, create_role_decl].compact.join(" ")
|
53
|
+
conn.exec stmt
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove all privileges from the given role
|
57
|
+
def clean(rolename)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Drop existing users. Return true if any role was dropped. Drop depending
|
61
|
+
# privileges and objects too if :cascade is true. Note that cascade only
|
62
|
+
# works if connected to the database where the privileges exist. The
|
63
|
+
# :silent option is used in tests - fix it somehow!
|
64
|
+
def drop(*rolenames, cascade: false, silent: false)
|
65
|
+
rolenames = Array(rolenames).flatten.compact.select { |role| exist?(role) }
|
66
|
+
return false if rolenames.empty?
|
67
|
+
rolenames_sql = PgConn.sql_idents(rolenames)
|
68
|
+
conn.exec "drop owned by #{rolenames_sql} cascade" if cascade
|
69
|
+
conn.exec "drop role #{rolenames_sql}"
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# List users. TODO Use RE instead of database argument. Also doc this shit
|
74
|
+
# FIXME: Depends on the <database>__<username> convention
|
75
|
+
def list(database: nil, owner: false, superuser: nil, can_login: nil)
|
76
|
+
database_clause = database && "rolname like '#{database}__%'"
|
77
|
+
database_clause = database && "(#{database_clause} or rolname = '#{database}')" if owner
|
78
|
+
superuser_clause = superuser.nil? ? nil : "rolsuper = #{superuser}"
|
79
|
+
can_login_clause = can_login.nil? ? nil : "rolcanlogin = #{can_login}"
|
80
|
+
query = [
|
81
|
+
"select rolname from pg_roles where true",
|
82
|
+
database_clause, superuser_clause, can_login_clause
|
83
|
+
].compact.join(" and ")
|
84
|
+
conn.values(query)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
@@ -0,0 +1,157 @@
|
|
1
|
+
|
2
|
+
module PgConn
|
3
|
+
# Schema methods
|
4
|
+
class SchemaMethods
|
5
|
+
attr_reader :conn
|
6
|
+
|
7
|
+
def initialize(conn)
|
8
|
+
@conn = conn
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return true if schema exists
|
12
|
+
def exist?(schema)
|
13
|
+
conn.exist? %(
|
14
|
+
select 1
|
15
|
+
from information_schema.schemata
|
16
|
+
where schema_name = '#{schema}'
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create a new schema. The authorization option can be used to set the
|
21
|
+
# owner of the schema
|
22
|
+
def create(schema, authorization: nil)
|
23
|
+
authorization_clause = authorization ? "authorization #{authorization}" : nil
|
24
|
+
stmt = ["create schema", schema, authorization_clause].compact.join(" ")
|
25
|
+
conn.exec stmt
|
26
|
+
end
|
27
|
+
|
28
|
+
# Drop schema
|
29
|
+
def drop(schema, cascade: false)
|
30
|
+
if cascade
|
31
|
+
conn.exec "drop schema if exists #{schema} cascade"
|
32
|
+
else
|
33
|
+
conn.exec "drop schema if exists #{schema}"
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# List schemas. By built-in schemas are not listed unless the :all option
|
39
|
+
# is true. The :exclude option can be used to exclude named schemas
|
40
|
+
def list(all: false, exclude: [])
|
41
|
+
conn.values(%(
|
42
|
+
select schema_name
|
43
|
+
from information_schema.schemata
|
44
|
+
)).select { |schema|
|
45
|
+
!exclude.include?(schema) && (all ? true : schema !~ /^pg_/ && schema != "information_schema")
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns true if relation (table or view) exists
|
50
|
+
def exist_relation?(schema, relation)
|
51
|
+
conn.exist? relation_exist_query(schema, relation)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return true if table exists
|
55
|
+
def exist_table?(schema, table)
|
56
|
+
conn.exist?(relation_exist_query(schema, table, kind: %w(r)))
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return true if view exists
|
60
|
+
def exist_view?(schema, view)
|
61
|
+
conn.exist? relation_exist_query(schema, view, kind: %w(v m))
|
62
|
+
end
|
63
|
+
|
64
|
+
# Return true if the column exists
|
65
|
+
def exist_column?(schema, relation, column)
|
66
|
+
conn.exist? column_exist_query(schema, relation, column)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return list of relations in the schema
|
70
|
+
def list_relations(schema)
|
71
|
+
conn.values relation_list_query(schema)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return list of tables in the schema
|
75
|
+
def list_tables(schema)
|
76
|
+
conn.values relation_list_query(schema, kind: %w(r))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return list of view in the schema
|
80
|
+
def list_views(schema)
|
81
|
+
conn.values relation_list_query(schema, kind: %w(v m))
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return a list of columns. If relation is defined, only columns from that
|
85
|
+
# relation are listed. Columns are returned as fully qualified names (eg.
|
86
|
+
# "schema.relation.column")
|
87
|
+
def list_columns(schema, relation = nil)
|
88
|
+
conn.values column_list_query(schema, relation)
|
89
|
+
end
|
90
|
+
|
91
|
+
def exist_function(schema, function, signature)
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
def list_functions(schema, function = nil)
|
96
|
+
raise NotImplementedError
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def relation_exist_query(schema, relation, kind: nil)
|
101
|
+
kind_sql_list = "'" + (kind.nil? ? %w(r v m) : Array(kind).flatten).join("', '") + "'"
|
102
|
+
%(
|
103
|
+
select 1
|
104
|
+
from pg_class
|
105
|
+
where relnamespace::regnamespace::text = '#{schema}'
|
106
|
+
and relname = '#{relation}'
|
107
|
+
and relkind in (#{kind_sql_list})
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
def relation_list_query(schema, kind: nil)
|
112
|
+
kind_sql_list = "'" + (kind.nil? ? %w(r v m) : Array(kind).flatten).join("', '") + "'"
|
113
|
+
%(
|
114
|
+
select relname
|
115
|
+
from pg_class
|
116
|
+
where relnamespace::regnamespace::text = '#{schema}'
|
117
|
+
and relkind in (#{kind_sql_list})
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def column_exist_query(schema, relation, column)
|
122
|
+
%(
|
123
|
+
select 1
|
124
|
+
from pg_class c
|
125
|
+
join pg_attribute a on a.attrelid = c.oid
|
126
|
+
where c.relnamespace::regnamespace::text = '#{schema}'
|
127
|
+
and c.relname = '#{relation}'
|
128
|
+
and a.attname = '#{column}'
|
129
|
+
and a.attnum > 0
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def column_list_query(schema, relation)
|
134
|
+
relation_clause = relation ? "relname = '#{relation}'" : nil
|
135
|
+
[
|
136
|
+
%(
|
137
|
+
select '#{schema}' || '.' || c.relname || '.' || a.attname
|
138
|
+
from pg_class c
|
139
|
+
join pg_attribute a on a.attrelid = c.oid
|
140
|
+
where relnamespace::regnamespace::text = '#{schema}'
|
141
|
+
and a.attnum > 0
|
142
|
+
),
|
143
|
+
relation_clause
|
144
|
+
].compact.join(" and ")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
|
157
|
+
|
data/lib/pg_conn.rb
ADDED
@@ -0,0 +1,677 @@
|
|
1
|
+
require "pg"
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
require "pg_conn/version"
|
5
|
+
require "pg_conn/role_methods"
|
6
|
+
require "pg_conn/schema_methods"
|
7
|
+
require "pg_conn/rdbms_methods"
|
8
|
+
|
9
|
+
module PgConn
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
# Can be raised in #transaction blocks to rollback changes
|
13
|
+
class Rollback < Error; end
|
14
|
+
|
15
|
+
# Return a PgConn::Connection object. TODO: A block argument
|
16
|
+
def self.new(*args, &block) Connection.new(*args, &block) end
|
17
|
+
|
18
|
+
# Make the PgConn module pretend it has PgConn instances
|
19
|
+
def self.===(element) element.is_a?(PgConn::Connection) or super end
|
20
|
+
|
21
|
+
# Returns a PgConn::Connection object (aka. a PgConn object). It's arguments
|
22
|
+
# can be an existing connection that will just be returned of a set of
|
23
|
+
# PgConn::Connection#initialize arguments that will be used to create a new
|
24
|
+
# PgConn::Connection object
|
25
|
+
def self.ensure(*args)
|
26
|
+
if args.size == 1 && args.first.is_a?(PgConn::Connection)
|
27
|
+
args.first
|
28
|
+
else
|
29
|
+
PgConn::Connection.new(*args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# All results from the database are converted into native Ruby types
|
34
|
+
class Connection
|
35
|
+
# Make PgConn::Connection pretend to be an instance of the PgConn module
|
36
|
+
def is_a?(klass) klass == PgConn or super end
|
37
|
+
|
38
|
+
# The PG::Connection object
|
39
|
+
attr_reader :pg_connection
|
40
|
+
|
41
|
+
# The class of column names (Symbol or String). Default is Symbol
|
42
|
+
attr_reader :field_name_class
|
43
|
+
|
44
|
+
# Name of user
|
45
|
+
def user() @pg_connection.user end
|
46
|
+
alias_method :username, :user # Obsolete
|
47
|
+
|
48
|
+
# Name of database
|
49
|
+
def name() @pg_connection.db end
|
50
|
+
alias_method :database, :name # Obsolete
|
51
|
+
|
52
|
+
# Database manipulation methods: #exist?, #create, #drop, #list. It is
|
53
|
+
# named 'rdbms' because #database is already defined
|
54
|
+
attr_reader :rdbms
|
55
|
+
|
56
|
+
# Role manipulation methods: #exist?, #create, #drop, #list
|
57
|
+
attr_reader :role
|
58
|
+
|
59
|
+
# Schema manipulation methods: #exist?, #create, #drop, #list, and
|
60
|
+
# #exist?/#list for relations/tables/views/columns
|
61
|
+
attr_reader :schema
|
62
|
+
|
63
|
+
# The transaction timestamp of the most recent SQL statement executed by
|
64
|
+
# #exec or #transaction block
|
65
|
+
attr_reader :timestamp
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
# Initialize a connection object and connect to the database. #initialize has five
|
70
|
+
# variations:
|
71
|
+
#
|
72
|
+
# initialize(dbname = nil, user = nil, field_name_class: Symbol)
|
73
|
+
# initialize(connection_hash, field_name_class: Symbol)
|
74
|
+
# initialize(connection_string, field_name_class: Symbol)
|
75
|
+
# initialize(host, port, dbname, user, password, field_name_class: Symbol)
|
76
|
+
# initialize(pg_connection_object)
|
77
|
+
#
|
78
|
+
# The possible keys of the connection hash are :host, :port, :dbname, :user,
|
79
|
+
# and :password. The connection string can either be a space-separated list
|
80
|
+
# of <key>=<value> pairs with the same keys as the hash, or a URI with the
|
81
|
+
# format 'postgres[ql]://[user[:password]@][host][:port][/name]
|
82
|
+
#
|
83
|
+
# The :field_name_class option controls the Ruby type of column names. It can be
|
84
|
+
# Symbol (the default) or String. The :timestamp option is used
|
85
|
+
# internally to set the timestamp for transactions
|
86
|
+
#
|
87
|
+
# The last variant is used to establish a PgConn from an existing
|
88
|
+
# connection. It doesn't change the connection settings and is not
|
89
|
+
# recommended except in cases where you want to piggyback on an existing
|
90
|
+
# connection (eg. a Rails connection)
|
91
|
+
#
|
92
|
+
# Note that the connection hash and the connection string may support more
|
93
|
+
# parameters than documented here. Consult
|
94
|
+
# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
95
|
+
# for the full list
|
96
|
+
#
|
97
|
+
def initialize(*args)
|
98
|
+
if args.last.is_a?(Hash)
|
99
|
+
@field_name_class = args.last.delete(:field_name_class) || Symbol
|
100
|
+
@timestamp = args.last.delete(:timestamp)
|
101
|
+
args.pop if args.last.empty?
|
102
|
+
else
|
103
|
+
@field_name_class = Symbol
|
104
|
+
end
|
105
|
+
|
106
|
+
# else # We assume that the current user is a postgres superuser
|
107
|
+
# @db = PgConn.new("template0")
|
108
|
+
|
109
|
+
using_existing_connection = false
|
110
|
+
@pg_connection =
|
111
|
+
if args.size == 0
|
112
|
+
make_connection
|
113
|
+
elsif args.size == 1
|
114
|
+
case arg = args.first
|
115
|
+
when PG::Connection
|
116
|
+
using_existing_connection = true
|
117
|
+
arg
|
118
|
+
when String
|
119
|
+
if arg =~ /=/
|
120
|
+
make_connection arg
|
121
|
+
elsif arg =~ /\//
|
122
|
+
make_connection arg
|
123
|
+
else
|
124
|
+
make_connection dbname: arg
|
125
|
+
end
|
126
|
+
when Hash
|
127
|
+
make_connection **arg
|
128
|
+
else
|
129
|
+
raise Error, "Illegal argument type: #{arg.class}"
|
130
|
+
end
|
131
|
+
elsif args.size == 2
|
132
|
+
make_connection dbname: args.first, user: args.last
|
133
|
+
elsif args.size == 5
|
134
|
+
make_connection args[0], args[1], nil, nil, args[2], args[3], args[4]
|
135
|
+
else
|
136
|
+
raise Error, "Illegal number of parameters: #{args.size}"
|
137
|
+
end
|
138
|
+
|
139
|
+
if !using_existing_connection
|
140
|
+
# Auto-convert to ruby types
|
141
|
+
type_map = PG::BasicTypeMapForResults.new(@pg_connection)
|
142
|
+
|
143
|
+
# Use String as default type. Kills 'Warning: no type cast defined for
|
144
|
+
# type "uuid" with oid 2950..' warnings
|
145
|
+
type_map.default_type_map = PG::TypeMapAllStrings.new
|
146
|
+
|
147
|
+
# Timestamp decoder
|
148
|
+
type_map.add_coder PG::TextDecoder::Timestamp.new( # Timestamp without time zone
|
149
|
+
oid: 1114,
|
150
|
+
flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC)
|
151
|
+
|
152
|
+
# Decode anonymous records but note that this is only useful to convert the
|
153
|
+
# outermost structure into an array, the elements are not decoded and are
|
154
|
+
# returned as strings. It is best to avoid anonymous records if possible
|
155
|
+
type_map.add_coder PG::TextDecoder::Record.new(
|
156
|
+
oid: 2249
|
157
|
+
)
|
158
|
+
|
159
|
+
@pg_connection.type_map_for_results = type_map
|
160
|
+
@pg_connection.field_name_type = @field_name_class.to_s.downcase.to_sym # Use symbol field names
|
161
|
+
@pg_connection.exec "set client_min_messages to warning;" # Silence warnings
|
162
|
+
end
|
163
|
+
|
164
|
+
@schema = SchemaMethods.new(self)
|
165
|
+
@role = RoleMethods.new(self)
|
166
|
+
@rdbms = RdbmsMethods.new(self)
|
167
|
+
@timestamp = nil
|
168
|
+
@savepoints = nil # Stack of savepoint names. Nil if no transaction in progress
|
169
|
+
end
|
170
|
+
|
171
|
+
# Close the database connection
|
172
|
+
def terminate()
|
173
|
+
@pg_connection.close if !@pg_connection.finished?
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.new(*args, &block)
|
177
|
+
if block_given?
|
178
|
+
begin
|
179
|
+
object = Connection.allocate
|
180
|
+
object.send(:initialize, *args)
|
181
|
+
yield(object) if object.pg_connection
|
182
|
+
ensure
|
183
|
+
object.terminate if object.pg_connection
|
184
|
+
end
|
185
|
+
else
|
186
|
+
super(*args)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# # Return true iff the query returns exactly one value
|
191
|
+
# def exist?(query) count(query) == 1 end
|
192
|
+
|
193
|
+
# :call-seq:
|
194
|
+
# exist?(query)
|
195
|
+
# exist?(table, id)
|
196
|
+
# eists?(table, where_clause)
|
197
|
+
#
|
198
|
+
# Return true iff the query returns exactly one value
|
199
|
+
def exist?(*args)
|
200
|
+
arg1, arg2 = *args
|
201
|
+
query =
|
202
|
+
case arg2
|
203
|
+
when Integer; "select from #{arg1} where id = #{arg2}"
|
204
|
+
when String; "select from #{arg1} where #{arg2}"
|
205
|
+
when NilClass; arg1
|
206
|
+
end
|
207
|
+
count(query) == 1
|
208
|
+
end
|
209
|
+
|
210
|
+
# :call-seq:
|
211
|
+
# count(query)
|
212
|
+
# count(table, where_clause = nil)
|
213
|
+
#
|
214
|
+
# Return true if the table or the result of the query is empty
|
215
|
+
def empty?(arg, where_clause = nil)
|
216
|
+
if arg =~ /\s/
|
217
|
+
count("select 1 from (#{arg}) as inner_query")
|
218
|
+
else
|
219
|
+
count("select 1 from #{arg}" + (where_clause ? " where #{where_clause}" : ""))
|
220
|
+
end == 1
|
221
|
+
end
|
222
|
+
|
223
|
+
# :call-seq:
|
224
|
+
# count(query)
|
225
|
+
# count(table_name, where_clause = nil)
|
226
|
+
#
|
227
|
+
# The number of records in the table or in the query
|
228
|
+
def count(arg, where_clause = nil)
|
229
|
+
if arg =~ /\s/
|
230
|
+
value("select count(*) from (#{arg}) as inner_query")
|
231
|
+
else
|
232
|
+
value("select count(*) from #{arg}" + (where_clause ? " where #{where_clause}" : ""))
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Return a single value. It is an error if the query doesn't return a
|
237
|
+
# single record with a single column. If :transaction is true, the query
|
238
|
+
# will be executed in a transaction and be committed it :commit is true
|
239
|
+
# (the default). This can be used in 'insert ... returning ...' statements
|
240
|
+
def value(query) #, transaction: false, commit: true)
|
241
|
+
r = pg_exec(query)
|
242
|
+
check_1c(r)
|
243
|
+
check_1r(r)
|
244
|
+
r.values[0][0]
|
245
|
+
end
|
246
|
+
|
247
|
+
# Like #value but returns nil if no record was found. It is still an error
|
248
|
+
# if the query returns more than one column
|
249
|
+
def value?(query) #, transaction: false, commit: true)
|
250
|
+
r = pg_exec(query)
|
251
|
+
check_1c(r)
|
252
|
+
return nil if r.ntuples == 0
|
253
|
+
check_1r(r)
|
254
|
+
r.values[0][0]
|
255
|
+
end
|
256
|
+
|
257
|
+
# Return an array of values. It is an error if the query returns records
|
258
|
+
# with more than one column. If :transaction is true, the query will be
|
259
|
+
# executed in a transaction and be committed it :commit is true (the
|
260
|
+
# default). This can be used in 'insert ... returning ...' statements
|
261
|
+
def values(query)
|
262
|
+
r = pg_exec(query)
|
263
|
+
check_1c(r)
|
264
|
+
r.column_values(0)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Return an array of column values. It is an error if the query returns
|
268
|
+
# more than one record. If :transaction is true, the query will be executed
|
269
|
+
# in a transaction and be committed it :commit is true (the default). This
|
270
|
+
# can be used in 'insert ... returning ...' statements
|
271
|
+
def tuple(query)
|
272
|
+
r = pg_exec(query)
|
273
|
+
check_1r(r)
|
274
|
+
r.values[0]
|
275
|
+
end
|
276
|
+
|
277
|
+
# Like #tuple but returns nil if no record was found
|
278
|
+
def tuple?(query)
|
279
|
+
r = pg_exec(query)
|
280
|
+
return nil if r.ntuples == 0
|
281
|
+
check_1r(r)
|
282
|
+
r.values[0]
|
283
|
+
end
|
284
|
+
|
285
|
+
# Return an array of tuples. If :transaction is true, the query will be
|
286
|
+
# executed in a transaction and be committed it :commit is true (the
|
287
|
+
# default). This can be used in 'insert ... returning ...' statements
|
288
|
+
def tuples(query)
|
289
|
+
pg_exec(query).values
|
290
|
+
end
|
291
|
+
|
292
|
+
# Return a single-element hash from column name to value. It is an error
|
293
|
+
# if the query returns more than one record or more than one column. Note
|
294
|
+
# that you will probably prefer to use #value instead when you expect only
|
295
|
+
# a single field
|
296
|
+
def field(query)
|
297
|
+
r = pg_exec(query)
|
298
|
+
check_1c(r)
|
299
|
+
check_1r(r)
|
300
|
+
r.tuple(0).to_h
|
301
|
+
end
|
302
|
+
|
303
|
+
# Like #field but returns nil if no record was found
|
304
|
+
def field?(query)
|
305
|
+
r = pg_exec(query)
|
306
|
+
check_1c(r)
|
307
|
+
return nil if r.ntuples == 0
|
308
|
+
check_1r(r)
|
309
|
+
r.tuple(0).to_h
|
310
|
+
end
|
311
|
+
|
312
|
+
# Return an array of single-element hashes from column name to value. It
|
313
|
+
# is an error if the query returns records with more than one column. Note
|
314
|
+
# that you will probably prefer to use #values instead when you expect only
|
315
|
+
# single-column records
|
316
|
+
def fields(query)
|
317
|
+
r = pg_exec(query)
|
318
|
+
check_1c(r)
|
319
|
+
r.each.to_a.map(&:to_h)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Return a hash from column name (a Symbol) to field value. It is an error if
|
323
|
+
# the query returns more than one record. It blows up if a column name is
|
324
|
+
# not a valid ruby symbol
|
325
|
+
def record(query)
|
326
|
+
r = pg_exec(query)
|
327
|
+
check_1r(r)
|
328
|
+
r.tuple(0).to_h
|
329
|
+
end
|
330
|
+
|
331
|
+
# Like #record but returns nil if no record was found
|
332
|
+
def record?(query)
|
333
|
+
r = pg_exec(query)
|
334
|
+
return nil if r.ntuples == 0
|
335
|
+
check_1r(r)
|
336
|
+
r.tuple(0).to_h
|
337
|
+
end
|
338
|
+
|
339
|
+
# Return an array of hashes from column name to field value
|
340
|
+
def records(query)
|
341
|
+
r = pg_exec(query)
|
342
|
+
r.each.to_a.map(&:to_h)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Return a record as a OpenStruct object. It is an error if the query
|
346
|
+
# returns more than one record. It blows up if a column name is not a valid
|
347
|
+
# ruby symbol
|
348
|
+
def struct(query)
|
349
|
+
OpenStruct.new(**record(query))
|
350
|
+
end
|
351
|
+
|
352
|
+
# Like #struct but returns nil if no record was found
|
353
|
+
def struct?(query)
|
354
|
+
args = record?(query)
|
355
|
+
return nil if args.nil?
|
356
|
+
OpenStruct.new(**args)
|
357
|
+
end
|
358
|
+
|
359
|
+
# Return an array of OpenStruct objects
|
360
|
+
def structs(query)
|
361
|
+
records(query).map { |record| OpenStruct.new(**record) }
|
362
|
+
end
|
363
|
+
|
364
|
+
# Return a hash from the record id column to record (hash from column name
|
365
|
+
# to field value) If the :key_column option is defined it will be used
|
366
|
+
# instead of id as the key It is an error if the id field value is not
|
367
|
+
# unique
|
368
|
+
def table(query, key_column: :id)
|
369
|
+
[String, Symbol].include?(key_column.class) or raise "Illegal key_column"
|
370
|
+
key_column = (field_name_class == Symbol ? key_column.to_sym : key_column.to_s)
|
371
|
+
r = pg_exec(query)
|
372
|
+
begin
|
373
|
+
r.fnumber(key_column.to_s) # FIXME: What is this?
|
374
|
+
rescue ArgumentError
|
375
|
+
raise Error, "Can't find column #{key_column}"
|
376
|
+
end
|
377
|
+
h = {}
|
378
|
+
r.each { |record|
|
379
|
+
key = record[key_column]
|
380
|
+
!h.key?(key) or raise Error, "Duplicate key: #{key.inspect}"
|
381
|
+
h[record[key_column]] = record.to_h
|
382
|
+
}
|
383
|
+
h
|
384
|
+
end
|
385
|
+
|
386
|
+
# Return a hash from the record id column to an OpenStruct representation
|
387
|
+
# of the record. If the :key_column option is defined it will be used
|
388
|
+
# instead of id as the key It is an error if the id field value is not
|
389
|
+
# unique
|
390
|
+
def set(query, key_column: :id)
|
391
|
+
key_column = key_column.to_sym
|
392
|
+
keys = {}
|
393
|
+
r = pg_exec(query)
|
394
|
+
begin
|
395
|
+
r.fnumber(key_column.to_s) # Check that key column exists
|
396
|
+
rescue ArgumentError
|
397
|
+
raise Error, "Can't find column #{key_column}"
|
398
|
+
end
|
399
|
+
h = {}
|
400
|
+
for i in 0...r.ntuples
|
401
|
+
struct = OpenStruct.new(**r[i])
|
402
|
+
key = struct.send(key_column)
|
403
|
+
!h.key?(key) or raise Error, "Duplicate key: #{key.inspect}"
|
404
|
+
h[key] = struct
|
405
|
+
end
|
406
|
+
h
|
407
|
+
end
|
408
|
+
|
409
|
+
# Returns a hash from the first field to a tuple of the remaining fields.
|
410
|
+
# If there is only one remaining field then that value is used instead of a
|
411
|
+
# tuple of that value. The optional +key+ argument sets the mapping field
|
412
|
+
def map(query, key = nil)
|
413
|
+
r = pg_exec(query)
|
414
|
+
begin
|
415
|
+
key = (key || r.fname(0)).to_s
|
416
|
+
key_index = r.fnumber(key.to_s)
|
417
|
+
one = (r.nfields == 2)
|
418
|
+
rescue ArgumentError
|
419
|
+
raise Error, "Can't find column #{key}"
|
420
|
+
end
|
421
|
+
h = {}
|
422
|
+
r.each_row { |row|
|
423
|
+
key_value = row.delete_at(key_index)
|
424
|
+
!h.key?(key_value) or raise Error, "Duplicate key: #{key_value}"
|
425
|
+
h[key_value] = (one ? row.first : row)
|
426
|
+
}
|
427
|
+
h
|
428
|
+
end
|
429
|
+
|
430
|
+
# Return the value of calling the given function (which can be a String or
|
431
|
+
# a Symbol). It dynamically detects the structure of the result and return
|
432
|
+
# a value or an array of values if the result contained only one column
|
433
|
+
# (like #value or #values), a tuple if the record has multiple columns
|
434
|
+
# (like #tuple), and an array of of tuples if the result contained more
|
435
|
+
# than one record with multiple columns (like #tuples)
|
436
|
+
def call(name, *args)
|
437
|
+
args_sql = args.map { |arg| # TODO: Use pg's encoder
|
438
|
+
case arg
|
439
|
+
when NilClass; "null"
|
440
|
+
when String; "'#{arg}'"
|
441
|
+
when Integer; arg
|
442
|
+
when TrueClass, FalseClass; arg
|
443
|
+
when Array; raise NotImplementedError
|
444
|
+
when Hash; raise NotImplementedError
|
445
|
+
else
|
446
|
+
raise ArgumentError, "Unrecognized value: #{arg.inspect}"
|
447
|
+
end
|
448
|
+
}.join(", ")
|
449
|
+
query = "select * from #{name}(#{args_sql})"
|
450
|
+
r = pg_exec(query)
|
451
|
+
if r.ntuples == 0
|
452
|
+
raise Error, "No records returned"
|
453
|
+
elsif r.ntuples == 1
|
454
|
+
if r.nfields == 1
|
455
|
+
r.values[0][0]
|
456
|
+
else
|
457
|
+
r.values[0]
|
458
|
+
end
|
459
|
+
elsif r.nfields == 1
|
460
|
+
r.column_values(0)
|
461
|
+
else
|
462
|
+
r.values
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
# Execute SQL statement(s) in a transaction and return the number of
|
467
|
+
# affected records (if any). Also sets #timestamp unless a transaction is
|
468
|
+
# already in progress. The +sql+ argument can be a String or an arbitrarily
|
469
|
+
# nested array of strings. The empty array is a NOP but the empty string is
|
470
|
+
# not.
|
471
|
+
#
|
472
|
+
# #exec pass Postgres exceptions to the caller unless :fail is false. If
|
473
|
+
# fail is false #exec instead return nil but note that postgres doesn't
|
474
|
+
# ignore it so that if you're inside a transaction, the transaction will be
|
475
|
+
# in an error state and if you're also using subtransactions the whole
|
476
|
+
# transaction stack collapses.
|
477
|
+
#
|
478
|
+
# TODO: Make sure the transaction stack is emptied on postgres errors
|
479
|
+
def exec(sql, commit: true, fail: true, silent: false)
|
480
|
+
transaction(commit: commit) { execute(sql, fail: fail, silent: silent) }
|
481
|
+
end
|
482
|
+
|
483
|
+
# Execute SQL statement(s) without a transaction block and return the
|
484
|
+
# number of affected records (if any). This used to call procedures that
|
485
|
+
# may manipulate transactions. The +sql+ argument can be a String or
|
486
|
+
# an arbitrarily nested array of strings. The empty array is a NOP but the
|
487
|
+
# empty string is not. #exec pass Postgres exceptions to the caller unless
|
488
|
+
# :fail is false in which case it returns nil
|
489
|
+
#
|
490
|
+
# TODO: Handle postgres exceptions wrt transaction state and stack
|
491
|
+
def execute(sql, fail: true, silent: false)
|
492
|
+
pg_exec(sql, fail: fail, silent: silent)&.cmd_tuples
|
493
|
+
end
|
494
|
+
|
495
|
+
# Switch user to the given user and execute the statement before swithcing
|
496
|
+
# back to the original user
|
497
|
+
#
|
498
|
+
# FIXME: The out-commented transaction block makes postspec fail for some reason
|
499
|
+
def su(username, &block)
|
500
|
+
raise Error, "Missing block in call to PgConn::Connection#su" if !block_given?
|
501
|
+
realuser = self.value "select current_user"
|
502
|
+
result = nil
|
503
|
+
# transaction(commit: false) {
|
504
|
+
execute "set session authorization #{username}"
|
505
|
+
result = yield
|
506
|
+
execute "set session authorization #{realuser}"
|
507
|
+
# }
|
508
|
+
result
|
509
|
+
end
|
510
|
+
|
511
|
+
# TODO: Move to TransactionMethods
|
512
|
+
|
513
|
+
def commit()
|
514
|
+
if transaction?
|
515
|
+
pop_transaction
|
516
|
+
else
|
517
|
+
pg_exec("commit")
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def rollback() raise Rollback end
|
522
|
+
|
523
|
+
# True if a transaction is in progress
|
524
|
+
def transaction?() !@savepoints.nil? end
|
525
|
+
|
526
|
+
# Returns number of transaction or savepoint levels
|
527
|
+
def transactions() @savepoints ? 1 + @savepoints.size : 0 end
|
528
|
+
|
529
|
+
def push_transaction
|
530
|
+
if transaction?
|
531
|
+
savepoint = "savepoint_#{@savepoints.size + 1}"
|
532
|
+
@savepoints.push savepoint
|
533
|
+
pg_exec("savepoint #{savepoint}")
|
534
|
+
else
|
535
|
+
@savepoints = []
|
536
|
+
pg_exec("begin")
|
537
|
+
@timestamp = pg_exec("select current_timestamp").values[0][0]
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def pop_transaction(commit: true)
|
542
|
+
transaction? or raise Error, "No transaction in progress"
|
543
|
+
if savepoint = @savepoints.pop
|
544
|
+
if !commit
|
545
|
+
pg_exec("rollback to savepoint #{savepoint}")
|
546
|
+
pg_exec("release savepoint #{savepoint}")
|
547
|
+
else
|
548
|
+
pg_exec("release savepoint #{savepoint}")
|
549
|
+
end
|
550
|
+
else
|
551
|
+
@savepoints = nil
|
552
|
+
pg_exec(commit ? "commit" : "rollback")
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
# Does a rollback and empties the stack. This should be called in response
|
557
|
+
# to PG::Error exceptions because then the whole transaction stack is
|
558
|
+
# invalid
|
559
|
+
def cancel_transaction
|
560
|
+
pg_exec("rollback")
|
561
|
+
@savepoints = nil
|
562
|
+
end
|
563
|
+
|
564
|
+
# Execute block within a transaction and return the result of the block.
|
565
|
+
# The transaction can be rolled back by raising a PgConn::Rollback
|
566
|
+
# exception in which case #transaction returns nil. Note that the
|
567
|
+
# transaction timestamp is set to the start of the first transaction even
|
568
|
+
# if transactions are nested
|
569
|
+
def transaction(commit: true, &block)
|
570
|
+
result = nil
|
571
|
+
begin
|
572
|
+
push_transaction
|
573
|
+
result = yield
|
574
|
+
rescue PgConn::Rollback
|
575
|
+
pop_trancaction(commit: false)
|
576
|
+
return nil
|
577
|
+
end
|
578
|
+
pop_transaction(commit: commit)
|
579
|
+
result
|
580
|
+
end
|
581
|
+
|
582
|
+
private
|
583
|
+
# Wrapper around PG::Connection.new that switches to the postgres user
|
584
|
+
# before connecting if the current user is the root user
|
585
|
+
#
|
586
|
+
def make_connection(*args, **opts)
|
587
|
+
if Process.euid == 0
|
588
|
+
begin
|
589
|
+
postgres_uid = Process::UID.from_name "postgres"
|
590
|
+
rescue ArgumentError
|
591
|
+
raise Error, "Can't find 'postgres' user"
|
592
|
+
end
|
593
|
+
begin
|
594
|
+
postgres_gid = Process::GID.from_name "postgres"
|
595
|
+
rescue ArgumentError
|
596
|
+
raise Error, "Can't find 'postgres' group"
|
597
|
+
end
|
598
|
+
|
599
|
+
begin
|
600
|
+
Process::Sys.seteuid postgres_uid
|
601
|
+
Process::Sys.setegid postgres_gid
|
602
|
+
PG::Connection.new *args, **opts
|
603
|
+
ensure
|
604
|
+
Process::Sys.seteuid 0
|
605
|
+
Process::Sys.setguid 0
|
606
|
+
end
|
607
|
+
else
|
608
|
+
PG::Connection.new *args, **opts
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
# :call-seq:
|
613
|
+
# pg_exec(string)
|
614
|
+
# pg_exec(array)
|
615
|
+
#
|
616
|
+
# +arg+ can be a statement or an array of statements. The statements are
|
617
|
+
# concatenated before being sent to the server. It returns a PG::Result
|
618
|
+
# object or nil if +arg+ was empty. #exec pass Postgres exceptions to the
|
619
|
+
# caller unless :fail is false
|
620
|
+
#
|
621
|
+
# FIXME: Error message prints the last statement but what if another
|
622
|
+
# statement failed?
|
623
|
+
#
|
624
|
+
# TODO WILD: Parse sql and split it into statements that are executed
|
625
|
+
# one-by-one so we're able to pinpoint errors in the source
|
626
|
+
#
|
627
|
+
# TODO: Fix silent by not handling exceptions
|
628
|
+
def pg_exec(arg, fail: true, silent: false)
|
629
|
+
begin
|
630
|
+
last_stmt = nil # To make the current SQL statement visible to the rescue clause
|
631
|
+
if arg.is_a?(String)
|
632
|
+
return nil if arg == ""
|
633
|
+
last_stmt = arg
|
634
|
+
@pg_connection.exec(last_stmt)
|
635
|
+
else
|
636
|
+
stmts = arg.flatten.compact
|
637
|
+
return nil if stmts.empty?
|
638
|
+
last_stmt = stmts.last
|
639
|
+
@pg_connection.exec(stmts.join(";\n"))
|
640
|
+
end
|
641
|
+
|
642
|
+
rescue PG::Error => ex
|
643
|
+
if fail
|
644
|
+
if !silent # FIXME Why do we handle this?
|
645
|
+
$stderr.puts arg
|
646
|
+
$stderr.puts ex.message
|
647
|
+
$stderr.flush
|
648
|
+
end
|
649
|
+
raise
|
650
|
+
else
|
651
|
+
return nil
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
def check_1c(r)
|
657
|
+
case r.nfields
|
658
|
+
when 0; raise Error, "No columns returned"
|
659
|
+
when 1;
|
660
|
+
else
|
661
|
+
raise Error, "More than one column returned"
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
def check_1r(r)
|
666
|
+
if r.ntuples == 0
|
667
|
+
raise Error, "No records returned"
|
668
|
+
elsif r.ntuples > 1
|
669
|
+
raise Error, "More than one record returned"
|
670
|
+
end
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
def self.sql_values(values) "'" + values.join("', '") + "'" end
|
675
|
+
def self.sql_idents(values) '"' + values.join('", "') + '"' end
|
676
|
+
end
|
677
|
+
|
data/pg_conn.gemspec
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/pg_conn/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "pg_conn"
|
7
|
+
spec.version = PgConn::VERSION
|
8
|
+
spec.authors = ["Claus Rasmussen"]
|
9
|
+
spec.email = ["claus.l.rasmussen@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Gem pg_conn"
|
12
|
+
spec.description = "Gem pg_conn"
|
13
|
+
spec.homepage = "http://www.nowhere.com/"
|
14
|
+
spec.required_ruby_version = ">= 2.4.0"
|
15
|
+
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
23
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
# Uncomment to register a new dependency of your gem
|
31
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
32
|
+
|
33
|
+
# For more information and examples about making a new gem, checkout our
|
34
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
35
|
+
|
36
|
+
# Add your production dependencies here
|
37
|
+
# spec.add_dependency GEM [, VERSION]
|
38
|
+
spec.add_dependency "pg"
|
39
|
+
|
40
|
+
# Add your development dependencies here
|
41
|
+
# spec.add_development_dependency GEM [, VERSION]
|
42
|
+
|
43
|
+
# Also un-comment in spec/spec_helper to use simplecov
|
44
|
+
# spec.add_development_dependency "simplecov"
|
45
|
+
|
46
|
+
# In development mode override load paths for gems whose source are located
|
47
|
+
# as siblings of this project directory
|
48
|
+
if File.directory?("#{__dir__}/.git")
|
49
|
+
local_projects = Dir["../*"].select { |path|
|
50
|
+
File.directory?(path) && File.exist?("#{path}/Gemfile")
|
51
|
+
}.map { |relpath| "#{File.absolute_path(relpath)}/lib" }
|
52
|
+
$LOAD_PATH.unshift *local_projects
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_conn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Claus Rasmussen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-02-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Gem pg_conn
|
28
|
+
email:
|
29
|
+
- claus.l.rasmussen@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rspec"
|
35
|
+
- ".ruby-version"
|
36
|
+
- Gemfile
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- TODO
|
40
|
+
- bin/console
|
41
|
+
- bin/setup
|
42
|
+
- lib/pg_conn.rb
|
43
|
+
- lib/pg_conn/rdbms_methods.rb
|
44
|
+
- lib/pg_conn/role_methods.rb
|
45
|
+
- lib/pg_conn/schema_methods.rb
|
46
|
+
- lib/pg_conn/version.rb
|
47
|
+
- pg_conn.gemspec
|
48
|
+
homepage: http://www.nowhere.com/
|
49
|
+
licenses: []
|
50
|
+
metadata:
|
51
|
+
homepage_uri: http://www.nowhere.com/
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 2.4.0
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.2.26
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Gem pg_conn
|
71
|
+
test_files: []
|