pgsnap 1.0.0.beta.10
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/.gitignore +8 -0
- data/.reek.yml +11 -0
- data/.rubocop.yml +14 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/lib/pgsnap.rb +23 -0
- data/lib/pgsnap/base_table.rb +25 -0
- data/lib/pgsnap/configuration.rb +30 -0
- data/lib/pgsnap/connection.rb +43 -0
- data/lib/pgsnap/nested_json.rb +72 -0
- data/lib/pgsnap/pg_result.rb +53 -0
- data/lib/pgsnap/query.rb +150 -0
- data/lib/pgsnap/select_command.rb +123 -0
- data/lib/pgsnap/table_columns_cache.rb +30 -0
- data/lib/pgsnap/utils.rb +63 -0
- data/lib/pgsnap/version.rb +5 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f23fdcee25f7ea505b0c471fe2757ad29d6a95491e1a952bd9d86dd517ded524
|
|
4
|
+
data.tar.gz: 6186c19247c3b871c57a1fce5a71bce1789960167643001bfe041d6e8627b89b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ab23995a482352b22bfa5564fa30faff1ba57f548f8a4bbad379cd7ebc6078bc562ec12c9660976a6172c2af44cb1b46778fbf1de58622c1a0ccd7c072018b56
|
|
7
|
+
data.tar.gz: 1d10672e506a839f9cbb84116faa216dee1dedef87ada3fe62cb42e3fdea5e9599d541e37fce097e074d1d2f390fead27916156a32af214fb4dbf221452eb8d8
|
data/.gitignore
ADDED
data/.reek.yml
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.5.3
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
|
File without changes
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 David Chin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Construct composable, structured, PostgreSQL queries in plain Ruby.
|
|
2
|
+
|
|
3
|
+
## Configure database connection
|
|
4
|
+
|
|
5
|
+
In a configuration file, named `config.rb` for example:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
module Queries
|
|
9
|
+
Pgsnap.set_configuration do |config|
|
|
10
|
+
config.dbname = 'your_db_name_here'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Config < Pgsnap::Query; end
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Inherit the configuration file in your query, named `query_for_one.rb`
|
|
18
|
+
for example:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
module Queries
|
|
22
|
+
class Hello < Queries::Config
|
|
23
|
+
def select_list
|
|
24
|
+
select_list_item %('hello'), :greeting
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Try it out in the console:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
pry(main)> q = Queries::Hello.new
|
|
34
|
+
pry(main)> q.result
|
|
35
|
+
#=> ["hello"]
|
|
36
|
+
pry(main)> q.columns
|
|
37
|
+
#=> [:greeting]
|
|
38
|
+
pry(main)> q.json
|
|
39
|
+
#=> [{"greeting"=>"hello"}]
|
|
40
|
+
```
|
data/lib/pgsnap.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pgsnap/version'
|
|
4
|
+
|
|
5
|
+
require 'pgsnap/configuration'
|
|
6
|
+
|
|
7
|
+
require 'pgsnap/utils'
|
|
8
|
+
|
|
9
|
+
require 'pgsnap/connection'
|
|
10
|
+
require 'pgsnap/base_table'
|
|
11
|
+
require 'pgsnap/nested_json'
|
|
12
|
+
require 'pgsnap/pg_result'
|
|
13
|
+
require 'pgsnap/select_command'
|
|
14
|
+
require 'pgsnap/query'
|
|
15
|
+
require 'pgsnap/table_columns_cache'
|
|
16
|
+
|
|
17
|
+
module Pgsnap
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
Pgsnap.set_configuration do |config|
|
|
21
|
+
config.dbname = 'pgsnap'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgsnap
|
|
4
|
+
class BaseTable
|
|
5
|
+
attr_reader :table_name
|
|
6
|
+
|
|
7
|
+
def initialize(table_name:)
|
|
8
|
+
@table_name = table_name.to_sym
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def columns
|
|
12
|
+
cached_columns || Pgsnap::TableColumnsCache.set do |cache|
|
|
13
|
+
cache[table_name] = Pgsnap::Query.new(
|
|
14
|
+
"SELECT * FROM #{table_name} WHERE false"
|
|
15
|
+
).columns
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def cached_columns
|
|
22
|
+
Pgsnap::TableColumnsCache.columns[table_name]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# http://lizabinante.com/blog/creating-a-configurable-ruby-gem/
|
|
4
|
+
module Pgsnap
|
|
5
|
+
class << self
|
|
6
|
+
# :reek:Attribute
|
|
7
|
+
attr_writer :configuration
|
|
8
|
+
|
|
9
|
+
def configuration
|
|
10
|
+
@configuration ||= Configuration.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset_configuration
|
|
14
|
+
@configuration = Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def set_configuration
|
|
18
|
+
yield(configuration)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Configuration
|
|
23
|
+
# :reek:Attribute
|
|
24
|
+
attr_accessor :dbname
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@dbname = nil # nil specifies that this config is required
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pg'
|
|
4
|
+
|
|
5
|
+
module Pgsnap
|
|
6
|
+
class Connection
|
|
7
|
+
attr_reader :connection, :dbname
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@dbname = Pgsnap.configuration.dbname
|
|
11
|
+
connect
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def connect
|
|
15
|
+
@connection = PG.connect(dbname: dbname).tap do |conn|
|
|
16
|
+
# # (A)
|
|
17
|
+
# # otherwise, all values are returned as strings
|
|
18
|
+
# # https://github.com/ged/ruby-pg#type-casts
|
|
19
|
+
# # got Warning: no type cast defined for type "xid" with oid 28.
|
|
20
|
+
# # Please cast this type explicitly to TEXTto be safe for future
|
|
21
|
+
# # changes.
|
|
22
|
+
# # => so, used (B)
|
|
23
|
+
# conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)
|
|
24
|
+
|
|
25
|
+
# (B)
|
|
26
|
+
# https://stackoverflow.com/questions/34795078/
|
|
27
|
+
# pg-gem-warning-no-type-cast-defined-for-type-numeric/
|
|
28
|
+
# 51882462#51882462
|
|
29
|
+
map = PG::BasicTypeMapForResults.new(conn)
|
|
30
|
+
map.default_type_map = PG::TypeMapAllStrings.new
|
|
31
|
+
conn.type_map_for_results = map
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dbname_from_status
|
|
36
|
+
status[0]['datname']
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def status
|
|
40
|
+
connection.exec('SELECT * FROM pg_stat_activity')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgsnap
|
|
4
|
+
class NestedJson
|
|
5
|
+
attr_reader :command
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@_u = Pgsnap::Utils
|
|
9
|
+
@command = {}
|
|
10
|
+
construct_command
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def nesting
|
|
14
|
+
@nesting ||= stringified_command
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def order_by_clause; end
|
|
18
|
+
|
|
19
|
+
def select_list_item(expression, expression_alias)
|
|
20
|
+
# handles 'table_name.*' expression
|
|
21
|
+
raise(
|
|
22
|
+
Error,
|
|
23
|
+
'select list item cannot be * columns in a nested JSON select command'
|
|
24
|
+
) if expression[/^.+\.(\*)$/, 1]
|
|
25
|
+
|
|
26
|
+
raise Error, 'expression_alias cannot be empty' unless expression_alias
|
|
27
|
+
|
|
28
|
+
select_list_struct << "#{expression} AS #{expression_alias}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sort(expression, direction = 'ASC')
|
|
32
|
+
order_by_clause_struct << [expression, direction.upcase]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :_u
|
|
38
|
+
|
|
39
|
+
def construct_command
|
|
40
|
+
select_list
|
|
41
|
+
order_by_clause
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def order_by_clause_struct
|
|
45
|
+
command[:order_by_clause] ||= []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def select_list_struct
|
|
49
|
+
command[:select_list] ||= []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stringified_command
|
|
53
|
+
a = _u.wrap_in_parentheses(
|
|
54
|
+
_u.build_clause('SELECT', select_list_struct, ',')
|
|
55
|
+
)
|
|
56
|
+
b = _u.join_with_space([
|
|
57
|
+
'SELECT x FROM',
|
|
58
|
+
a,
|
|
59
|
+
'AS x'
|
|
60
|
+
].compact)
|
|
61
|
+
c = _u.wrap_in_parentheses(b)
|
|
62
|
+
_u.join_with_space([
|
|
63
|
+
c,
|
|
64
|
+
_u.build_clause(
|
|
65
|
+
'ORDER BY',
|
|
66
|
+
order_by_clause_struct.map { |item| item.join(' ') },
|
|
67
|
+
','
|
|
68
|
+
)
|
|
69
|
+
].compact)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgsnap
|
|
4
|
+
class PgResult
|
|
5
|
+
|
|
6
|
+
def initialize(select_command)
|
|
7
|
+
@select_command = select_command
|
|
8
|
+
set_pg_result
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def columns
|
|
12
|
+
(0..(number_of_columns - 1)).map { |idx| column_name(idx) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def number_of_columns
|
|
16
|
+
pg_result.nfields
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def number_of_rows
|
|
20
|
+
pg_result.ntuples
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def array_of_hashes
|
|
24
|
+
native.first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def native
|
|
28
|
+
number_of_rows == 1 ? values.first : values
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def nested_json_result
|
|
32
|
+
native.first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def values
|
|
36
|
+
pg_result.values
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :pg_result, :select_command
|
|
42
|
+
|
|
43
|
+
def column_name(idx)
|
|
44
|
+
pg_result.fname(idx).to_sym
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def set_pg_result
|
|
48
|
+
pg_result || @pg_result = Pgsnap::Connection.new.connection.exec(
|
|
49
|
+
select_command
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/pgsnap/query.rb
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Pgsnap
|
|
6
|
+
# :reek:TooManyMethods
|
|
7
|
+
class Query
|
|
8
|
+
def initialize
|
|
9
|
+
construct_command
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Results
|
|
13
|
+
##
|
|
14
|
+
|
|
15
|
+
# Executes a new instance of query on PostgreSQL
|
|
16
|
+
def json
|
|
17
|
+
pgresult_instance(select_command_json).array_of_hashes
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def json_string
|
|
21
|
+
json.to_json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Executes a new instance of query on PostgreSQL
|
|
25
|
+
def result
|
|
26
|
+
pgresult_instance(select_command).native
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# SELECT commands
|
|
30
|
+
##
|
|
31
|
+
|
|
32
|
+
def as_subquery
|
|
33
|
+
_u.wrap_in_parentheses(select_command)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def select_command
|
|
37
|
+
_c.select_command
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def select_command_json
|
|
41
|
+
_c.select_command_json
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def select_command_json_string
|
|
45
|
+
_c.select_command_json_string
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Metadata
|
|
50
|
+
##
|
|
51
|
+
|
|
52
|
+
# command struct
|
|
53
|
+
def command
|
|
54
|
+
_c.command
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def columns
|
|
58
|
+
raise Error, 'Query has not executed yet' unless pgresult
|
|
59
|
+
|
|
60
|
+
pgresult.columns
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dbname
|
|
64
|
+
Pgsnap.configuration.dbname
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Commands
|
|
68
|
+
##
|
|
69
|
+
|
|
70
|
+
def from(table_reference, table_reference_alias)
|
|
71
|
+
_c.from(table_reference, table_reference_alias)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inner_join(table_reference, table_reference_alias, on:)
|
|
75
|
+
_c.inner_join(table_reference, table_reference_alias, on)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def group(group_expression)
|
|
79
|
+
_c.group(group_expression)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def json_agg(nesting_definition)
|
|
83
|
+
_c.json_agg(nesting_definition)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def limit(number_of_rows)
|
|
87
|
+
_c.limit(number_of_rows)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def literal(query_string)
|
|
91
|
+
_c.literal(query_string)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def relation(query_class, relation_alias = nil)
|
|
95
|
+
_c.relation(query_class, relation_alias)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def select_list_item(expression, expression_alias = nil)
|
|
99
|
+
_c.select_list_item(expression, expression_alias)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def sort(sort_expression, direction = 'ASC')
|
|
103
|
+
_c.sort(sort_expression, direction)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def values(*expression)
|
|
107
|
+
_c.values(*expression)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
attr_reader :pgresult
|
|
113
|
+
|
|
114
|
+
# constructs query from methods defined in subclass
|
|
115
|
+
def construct_command
|
|
116
|
+
select_list
|
|
117
|
+
table_expression
|
|
118
|
+
group_by_clause
|
|
119
|
+
order_by_clause
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# optional
|
|
123
|
+
##
|
|
124
|
+
|
|
125
|
+
def group_by_clause; end
|
|
126
|
+
|
|
127
|
+
def order_by_clause; end
|
|
128
|
+
|
|
129
|
+
def table_expression; end
|
|
130
|
+
|
|
131
|
+
# aliases
|
|
132
|
+
##
|
|
133
|
+
|
|
134
|
+
def _c
|
|
135
|
+
@_c ||= Pgsnap::SelectCommand.new
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def _u
|
|
139
|
+
@_u ||= Pgsnap::Utils
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# metadata
|
|
143
|
+
##
|
|
144
|
+
|
|
145
|
+
def pgresult_instance(select_command)
|
|
146
|
+
@pgresult = Pgsnap::PgResult.new(select_command)
|
|
147
|
+
pgresult
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgsnap
|
|
4
|
+
# :reek:TooManyMethods
|
|
5
|
+
class SelectCommand
|
|
6
|
+
attr_reader :command
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@_u = Pgsnap::Utils
|
|
10
|
+
@command = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# SELECT commands
|
|
14
|
+
##
|
|
15
|
+
|
|
16
|
+
def select_command
|
|
17
|
+
@select_command ||= literal_query_string || stringified_command
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def select_command_json
|
|
21
|
+
"SELECT JSON_AGG(relation) FROM (#{select_command}) relation"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Commands
|
|
25
|
+
|
|
26
|
+
def from(table_reference, table_reference_alias)
|
|
27
|
+
table_expression_struct <<
|
|
28
|
+
"#{table_reference} AS #{table_reference_alias}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def group(expression)
|
|
32
|
+
group_by_clause_struct << expression
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def inner_join(table_reference, table_reference_alias, on)
|
|
36
|
+
table_expression_struct <<
|
|
37
|
+
"INNER JOIN #{table_reference} AS #{table_reference_alias} ON #{on}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def limit(number_of_rows)
|
|
41
|
+
limit_clause_struct << number_of_rows
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def literal(query_string)
|
|
45
|
+
literal_query_string_struct << query_string
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def json_agg(nesting_definition)
|
|
49
|
+
"JSON_AGG(#{nesting_definition})"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def relation(query_class, relation_alias)
|
|
53
|
+
cmd.relation(query_class, relation_alias)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def select_list_item(expression, expression_alias)
|
|
57
|
+
# handles 'table_name.*' expression
|
|
58
|
+
return select_list_struct << expression if expression[/^.+\.(\*)$/, 1]
|
|
59
|
+
|
|
60
|
+
raise Error, 'expression_alias cannot be empty' unless expression_alias
|
|
61
|
+
|
|
62
|
+
select_list_struct << "#{expression} AS #{expression_alias}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sort(expression, direction = 'ASC')
|
|
66
|
+
order_by_clause_struct << [expression, direction.upcase]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def values(*expression)
|
|
70
|
+
squished = _u.squish(expression)
|
|
71
|
+
wrapped_in_single_quotes = _u.wrap_in_single_quotes(squished)
|
|
72
|
+
comma_joined = _u.join_with_comma(wrapped_in_single_quotes)
|
|
73
|
+
"(VALUES(#{comma_joined}))"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
attr_reader :_u
|
|
79
|
+
|
|
80
|
+
def limit_clause_struct
|
|
81
|
+
command[:limit_clause] ||= []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def literal_query_string
|
|
85
|
+
command[:literal_query_string].join if
|
|
86
|
+
literal_query_string_struct.size > 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def literal_query_string_struct
|
|
90
|
+
command[:literal_query_string] ||= []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def group_by_clause_struct
|
|
94
|
+
command[:group_by_clause] ||= []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def order_by_clause_struct
|
|
98
|
+
command[:order_by_clause] ||= []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def select_list_struct
|
|
102
|
+
command[:select_list] ||= []
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def stringified_command
|
|
106
|
+
_u.join_with_space([
|
|
107
|
+
_u.build_clause('SELECT', select_list_struct, ','),
|
|
108
|
+
_u.build_clause('FROM', table_expression_struct, ' '),
|
|
109
|
+
_u.build_clause('GROUP BY', group_by_clause_struct, ','),
|
|
110
|
+
_u.build_clause(
|
|
111
|
+
'ORDER BY',
|
|
112
|
+
order_by_clause_struct.map { |item| item.join(' ') },
|
|
113
|
+
','
|
|
114
|
+
),
|
|
115
|
+
_u.build_scalar('LIMIT', limit_clause_struct.first)
|
|
116
|
+
].compact)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def table_expression_struct
|
|
120
|
+
command[:table_expression] ||= []
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# caches table columns queried from the database
|
|
4
|
+
module Pgsnap
|
|
5
|
+
class TableColumnsCache
|
|
6
|
+
class << self
|
|
7
|
+
# :reek:Attribute
|
|
8
|
+
attr_writer :cache
|
|
9
|
+
|
|
10
|
+
def cache
|
|
11
|
+
@cache ||= new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def columns
|
|
15
|
+
cache.columns
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set
|
|
19
|
+
yield(cache.columns)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# :reek:Attribute
|
|
24
|
+
attr_accessor :columns
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@columns = {}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/pgsnap/utils.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgsnap
|
|
4
|
+
module Utils
|
|
5
|
+
class << self
|
|
6
|
+
def build_clause(clause, content_array, separator)
|
|
7
|
+
return unless content_array.length.positive?
|
|
8
|
+
|
|
9
|
+
pretty_separator = separator == ' ' ? separator : "#{separator} "
|
|
10
|
+
"#{clause} #{content_array.join(pretty_separator)}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_scalar(clause, content)
|
|
14
|
+
"#{clause} #{content}" if content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ' i wanna be squished'
|
|
18
|
+
# => "i wanna be squished"
|
|
19
|
+
#
|
|
20
|
+
# [1, ' i wanna be squished']
|
|
21
|
+
# => [1, "i wanna be squished"]
|
|
22
|
+
def squish(content)
|
|
23
|
+
return content.split.join(' ') if content.is_a?(String)
|
|
24
|
+
|
|
25
|
+
return content.map { |elem| squish(elem) } if content.is_a?(Array)
|
|
26
|
+
|
|
27
|
+
content
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def wrap_in_parentheses(content)
|
|
31
|
+
"(#{content})"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# [1, 'i wanna be wrapped in single quotes']
|
|
35
|
+
# => [1, "'i wanna be wrapped in single quotes'"]
|
|
36
|
+
def wrap_in_single_quotes(content)
|
|
37
|
+
return %('#{content}') if content.is_a?(String)
|
|
38
|
+
|
|
39
|
+
if content.is_a?(Array)
|
|
40
|
+
return content.map { |elem| wrap_in_single_quotes(elem) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
content
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# [1, "'i wanna be wrapped in single quotes'"]
|
|
47
|
+
# => "1, 'i wanna be wrapped in single quotes'"
|
|
48
|
+
def join_with_comma(content)
|
|
49
|
+
return content.join(', ') if content.is_a?(Array)
|
|
50
|
+
|
|
51
|
+
content
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# [1, "'i wanna be wrapped in single quotes'"]
|
|
55
|
+
# => "1, 'i wanna be wrapped in single quotes'"
|
|
56
|
+
def join_with_space(content)
|
|
57
|
+
return content.join(' ') if content.is_a?(Array)
|
|
58
|
+
|
|
59
|
+
content
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pgsnap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0.beta.10
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Chin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-12-29 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: '1.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.1'
|
|
27
|
+
description: Construct composable, structured, PostgreSQL queries in plain Ruby.
|
|
28
|
+
email:
|
|
29
|
+
- dlcmhd@me.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".gitignore"
|
|
35
|
+
- ".reek.yml"
|
|
36
|
+
- ".rubocop.yml"
|
|
37
|
+
- ".ruby-version"
|
|
38
|
+
- ".travis.yml"
|
|
39
|
+
- CHANGELOG.md
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- lib/pgsnap.rb
|
|
43
|
+
- lib/pgsnap/base_table.rb
|
|
44
|
+
- lib/pgsnap/configuration.rb
|
|
45
|
+
- lib/pgsnap/connection.rb
|
|
46
|
+
- lib/pgsnap/nested_json.rb
|
|
47
|
+
- lib/pgsnap/pg_result.rb
|
|
48
|
+
- lib/pgsnap/query.rb
|
|
49
|
+
- lib/pgsnap/select_command.rb
|
|
50
|
+
- lib/pgsnap/table_columns_cache.rb
|
|
51
|
+
- lib/pgsnap/utils.rb
|
|
52
|
+
- lib/pgsnap/version.rb
|
|
53
|
+
homepage: https://pgsnap-ruby.briefnotes.net
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://pgsnap-ruby.briefnotes.net
|
|
58
|
+
source_code_uri: https://github.com/dlcmh/pgsnap-ruby
|
|
59
|
+
changelog_uri: https://github.com/dlcmh/pgsnap-ruby/blob/master/CHANGELOG.md
|
|
60
|
+
post_install_message:
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 2.0.0
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 1.3.1
|
|
74
|
+
requirements: []
|
|
75
|
+
rubyforge_project:
|
|
76
|
+
rubygems_version: 2.7.6
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Pgsnap makes working with PostgreSQL queries infinitely more pleasant.
|
|
80
|
+
test_files: []
|