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 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: []