pg_conn 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.7.1
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in pg_conn.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module PgConn
2
+ VERSION = "0.2.1"
3
+ end
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: []