torque-postgresql 2.2.4 → 2.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95fa82a4869d180b12518b184191e6877b9e6cf3108fb5e68e55d6a836809389
4
- data.tar.gz: 45c6e24a30b3782ec26ff5caafe01430c24f137c460eb19b6f2a49e0b6609142
3
+ metadata.gz: 51c2e056f8b1a5bfa52339dbc7212a03fdd7f99f7fa05b11d066859ef1763f45
4
+ data.tar.gz: 6b0cdc4ac793d578cc925363f331f167c80a73a1d708f68c98b2d8430b21331f
5
5
  SHA512:
6
- metadata.gz: cd7e925ab13b8ae3eb6f0d92fbb994f12c6c121998792263b0752eb9647c0d2e4f72dec8d3973c6e0365fe0c5194a2af5fba9189189743c5bca7b06a4e6dfa41
7
- data.tar.gz: 470bb4e8b0816318ca5ee1b99e1896a8a4f3297af46e653de4ddac5347cf40a781b530a4e2531cf41eae38f7205332e52ced09e5b857a618f965851a6d9d7162
6
+ metadata.gz: fd702676e2677393a1002c411380cb297f7f56b7f2bdad7317a446849accf3090b9dcd27d6b2e50b7e7c81d6299db97f3fbd190a6c21d6afca9782c575e900ba
7
+ data.tar.gz: 74cbc3c504fad7ebd55249ed4339c7a2571320828faebe7a2f573b759e721febd07e05c5227759748dbde4c0e629521d167d149e65339cd3a0be855f60076b29
data/README.rdoc CHANGED
@@ -128,6 +128,23 @@ reconfigured on the model, and then can be used during querying process.
128
128
 
129
129
  {Learn more}[link:classes/Torque/PostgreSQL/AuxiliaryStatement.html]
130
130
 
131
+ * Multiple Schemas
132
+
133
+ Allows models and modules to have a schema associated with them, so that
134
+ developers can better organize their tables into schemas and build features in
135
+ a way that the database can better represent how they are separated.
136
+
137
+ create_schema "internal", force: :cascade
138
+
139
+ module Internal
140
+ class User < ActiveRecord::Base
141
+ self.schema = 'internal'
142
+ end
143
+ end
144
+
145
+ Internal::User.all
146
+
147
+ {Learn more}[link:classes/Torque/PostgreSQL/Adapter/DatabaseStatements.html]
131
148
 
132
149
  == Download and installation
133
150
 
@@ -12,6 +12,26 @@ module Torque
12
12
  @_dump_mode = !!!@_dump_mode
13
13
  end
14
14
 
15
+ # List of schemas blocked by the application in the current connection
16
+ def schemas_blacklist
17
+ @schemas_blacklist ||= Torque::PostgreSQL.config.schemas.blacklist +
18
+ (@config.dig(:schemas, 'blacklist') || [])
19
+ end
20
+
21
+ # List of schemas used by the application in the current connection
22
+ def schemas_whitelist
23
+ @schemas_whitelist ||= Torque::PostgreSQL.config.schemas.whitelist +
24
+ (@config.dig(:schemas, 'whitelist') || [])
25
+ end
26
+
27
+ # A list of schemas on the search path sanitized
28
+ def schemas_search_path_sanitized
29
+ @schemas_search_path_sanitized ||= begin
30
+ db_user = @config[:username] || ENV['USER'] || ENV['USERNAME']
31
+ schema_search_path.split(',').map { |item| item.strip.sub('"$user"', db_user) }
32
+ end
33
+ end
34
+
15
35
  # Check if a given type is valid.
16
36
  def valid_type?(type)
17
37
  super || extended_types.include?(type)
@@ -22,6 +42,17 @@ module Torque
22
42
  EXTENDED_DATABASE_TYPES
23
43
  end
24
44
 
45
+ # Checks if a given schema exists in the database. If +filtered+ is
46
+ # given as false, then it will check regardless of whitelist and
47
+ # blacklist
48
+ def schema_exists?(name, filtered: true)
49
+ return user_defined_schemas.include?(name.to_s) if filtered
50
+
51
+ query_value(<<-SQL) == 1
52
+ SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = '#{name}'
53
+ SQL
54
+ end
55
+
25
56
  # Returns true if type exists.
26
57
  def type_exists?(name)
27
58
  user_defined_types.key? name.to_s
@@ -115,18 +146,41 @@ module Torque
115
146
  # Get the list of inherited tables associated with their parent tables
116
147
  def inherited_tables
117
148
  tables = query(<<-SQL, 'SCHEMA')
118
- SELECT child.relname AS table_name,
119
- array_agg(parent.relname) AS inheritances
149
+ SELECT inhrelid::regclass AS table_name,
150
+ inhparent::regclass AS inheritances
120
151
  FROM pg_inherits
121
152
  JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
122
153
  JOIN pg_class child ON pg_inherits.inhrelid = child.oid
123
- GROUP BY child.relname, pg_inherits.inhrelid
124
- ORDER BY pg_inherits.inhrelid
154
+ ORDER BY inhrelid
125
155
  SQL
126
156
 
127
- tables.map do |(table, refs)|
128
- [table, PG::TextDecoder::Array.new.decode(refs)]
129
- end.to_h
157
+ tables.each_with_object({}) do |(child, parent), result|
158
+ (result[child] ||= []) << parent
159
+ end
160
+ end
161
+
162
+ # Get the list of schemas that were created by the user
163
+ def user_defined_schemas
164
+ query_values(user_defined_schemas_sql, 'SCHEMA')
165
+ end
166
+
167
+ # Build the query for allowed schemas
168
+ def user_defined_schemas_sql
169
+ conditions = []
170
+ conditions << <<-SQL if schemas_blacklist.any?
171
+ nspname NOT LIKE ANY (ARRAY['#{schemas_blacklist.join("', '")}'])
172
+ SQL
173
+
174
+ conditions << <<-SQL if schemas_whitelist.any?
175
+ nspname LIKE ANY (ARRAY['#{schemas_whitelist.join("', '")}'])
176
+ SQL
177
+
178
+ <<-SQL.squish
179
+ SELECT nspname
180
+ FROM pg_catalog.pg_namespace
181
+ WHERE 1=1 AND #{conditions.join(' AND ')}
182
+ ORDER BY oid
183
+ SQL
130
184
  end
131
185
 
132
186
  # Get the list of columns, and their definition, but only from the
@@ -14,6 +14,7 @@ module Torque
14
14
 
15
15
  def extensions(stream) # :nodoc:
16
16
  super
17
+ user_defined_schemas(stream)
17
18
  user_defined_types(stream)
18
19
  end
19
20
 
@@ -41,7 +42,9 @@ module Torque
41
42
 
42
43
  def tables(stream) # :nodoc:
43
44
  inherited_tables = @connection.inherited_tables
44
- sorted_tables = @connection.tables.sort - @connection.views
45
+ sorted_tables = (@connection.tables - @connection.views).sort_by do |table_name|
46
+ table_name.split(/(?:public)?\./).reverse
47
+ end
45
48
 
46
49
  stream.puts " # These are the common tables"
47
50
  (sorted_tables - inherited_tables.keys).each do |table_name|
@@ -58,7 +61,7 @@ module Torque
58
61
 
59
62
  # Add the inherits setting
60
63
  sub_stream.rewind
61
- inherits.map!(&:to_sym)
64
+ inherits.map! { |parent| parent.to_s.sub(/\Apublic\./, '') }
62
65
  inherits = inherits.first if inherits.size === 1
63
66
  inherits = ", inherits: #{inherits.inspect} do |t|"
64
67
  table_dump = sub_stream.read.gsub(/ do \|t\|$/, inherits)
@@ -84,6 +87,20 @@ module Torque
84
87
  triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
85
88
  end
86
89
 
90
+ # Make sure to remove the schema from the table name
91
+ def remove_prefix_and_suffix(table)
92
+ super(table.sub(/\A[a-z0-9_]*\./, ''))
93
+ end
94
+
95
+ # Dump user defined schemas
96
+ def user_defined_schemas(stream)
97
+ return if (list = (@connection.user_defined_schemas - ['public'])).empty?
98
+
99
+ stream.puts " # Custom schemas defined in this database."
100
+ list.each { |name| stream.puts " create_schema \"#{name}\", force: :cascade" }
101
+ stream.puts
102
+ end
103
+
87
104
  # Dump user defined types like enum
88
105
  def user_defined_types(stream)
89
106
  types = @connection.user_defined_types('e')
@@ -7,6 +7,21 @@ module Torque
7
7
 
8
8
  TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
9
9
 
10
+ # Create a new schema
11
+ def create_schema(name, options = {})
12
+ drop_schema(name, options) if options[:force]
13
+
14
+ check = 'IF NOT EXISTS' if options.fetch(:check, true)
15
+ execute("CREATE SCHEMA #{check} #{quote_schema_name(name.to_s)}")
16
+ end
17
+
18
+ # Drop an existing schema
19
+ def drop_schema(name, options = {})
20
+ force = options.fetch(:force, '').upcase
21
+ check = 'IF EXISTS' if options.fetch(:check, true)
22
+ execute("DROP SCHEMA #{check} #{quote_schema_name(name.to_s)} #{force}")
23
+ end
24
+
10
25
  # Drops a type.
11
26
  def drop_type(name, options = {})
12
27
  force = options.fetch(:force, '').upcase
@@ -79,12 +94,37 @@ module Torque
79
94
 
80
95
  # Rewrite the method that creates tables to easily accept extra options
81
96
  def create_table(table_name, **options, &block)
97
+ table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
98
+
82
99
  options[:id] = false if options[:inherits].present? &&
83
100
  options[:primary_key].blank? && options[:id].blank?
84
101
 
85
102
  super table_name, **options, &block
86
103
  end
87
104
 
105
+ # Add the schema option when extracting table options
106
+ def table_options(table_name)
107
+ parts = table_name.split('.').reverse
108
+ return super unless parts.size == 2 && parts[1] != 'public'
109
+
110
+ (super || {}).merge(schema: parts[1])
111
+ end
112
+
113
+ # When dumping the schema we need to add all schemas, not only those
114
+ # active for the current +schema_search_path+
115
+ def quoted_scope(name = nil, type: nil)
116
+ return super unless name.nil?
117
+
118
+ super.merge(schema: "ANY ('{#{user_defined_schemas.join(',')}}')")
119
+ end
120
+
121
+ # Fix the query to include the schema on tables names when dumping
122
+ def data_source_sql(name = nil, type: nil)
123
+ return super unless name.nil?
124
+
125
+ super.sub('SELECT c.relname FROM', "SELECT n.nspname || '.' || c.relname FROM")
126
+ end
127
+
88
128
  private
89
129
 
90
130
  def quote_enum_values(name, values, options)
@@ -31,9 +31,9 @@ module Torque
31
31
  )
32
32
  end
33
33
 
34
- # Add `inherits` to the list of extracted table options
34
+ # Add `inherits` and `schema` to the list of extracted table options
35
35
  def extract_table_options!(options)
36
- super.merge(options.extract!(:inherits))
36
+ super.merge(options.extract!(:inherits, :schema))
37
37
  end
38
38
 
39
39
  # Allow filtered bulk insert by adding the where clause. This method is
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ class AuxiliaryStatement
6
+ class Recursive < AuxiliaryStatement
7
+ # Setup any additional option in the recursive mode
8
+ def initialize(*, **options)
9
+ super
10
+
11
+ @connect = options[:connect]&.to_a&.first
12
+ @union_all = options[:union_all]
13
+ @sub_query = options[:sub_query]
14
+
15
+ if options.key?(:with_depth)
16
+ @depth = options[:with_depth].values_at(:name, :start, :as)
17
+ @depth[0] ||= 'depth'
18
+ end
19
+
20
+ if options.key?(:with_path)
21
+ @path = options[:with_path].values_at(:name, :source, :as)
22
+ @path[0] ||= 'path'
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Build the string or arel query
29
+ def build_query(base)
30
+ # Expose columns and get the list of the ones for select
31
+ columns = expose_columns(base, @query.try(:arel_table))
32
+ sub_columns = columns.dup
33
+ type = @union_all.present? ? 'all' : ''
34
+
35
+ # Build any extra columns that are dynamic and from the recursion
36
+ extra_columns(base, columns, sub_columns)
37
+
38
+ # Prepare the query depending on its type
39
+ if @query.is_a?(String) && @sub_query.is_a?(String)
40
+ args = @args.each_with_object({}) { |h, (k, v)| h[k] = base.connection.quote(v) }
41
+ ::Arel.sql("(#{@query} UNION #{type.upcase} #{@sub_query})" % args)
42
+ elsif relation_query?(@query)
43
+ @query = @query.where(@where) if @where.present?
44
+ @bound_attributes.concat(@query.send(:bound_attributes))
45
+
46
+ if relation_query?(@sub_query)
47
+ @bound_attributes.concat(@sub_query.send(:bound_attributes))
48
+
49
+ sub_query = @sub_query.select(*sub_columns).arel
50
+ sub_query.from([@sub_query.arel_table, table])
51
+ else
52
+ sub_query = ::Arel.sql(@sub_query)
53
+ end
54
+
55
+ @query.select(*columns).arel.union(type, sub_query)
56
+ else
57
+ raise ArgumentError, <<-MSG.squish
58
+ Only String and ActiveRecord::Base objects are accepted as query and sub query
59
+ objects, #{@query.class.name} given for #{self.class.name}.
60
+ MSG
61
+ end
62
+ end
63
+
64
+ # Setup the statement using the class configuration
65
+ def prepare(base, settings)
66
+ super
67
+
68
+ prepare_sub_query(base, settings)
69
+ end
70
+
71
+ # Make sure that both parts of the union are ready
72
+ def prepare_sub_query(base, settings)
73
+ @union_all = settings.union_all if @union_all.nil?
74
+ @sub_query ||= settings.sub_query
75
+ @depth ||= settings.depth
76
+ @path ||= settings.path
77
+
78
+ # Collect the connection
79
+ @connect ||= settings.connect || begin
80
+ key = base.primary_key
81
+ [key.to_sym, :"parent_#{key}"] unless key.nil?
82
+ end
83
+
84
+ raise ArgumentError, <<-MSG.squish if @sub_query.nil? && @query.is_a?(String)
85
+ Unable to generate sub query from a string query. Please provide a `sub_query`
86
+ property on the "#{table_name}" settings.
87
+ MSG
88
+
89
+ if @sub_query.nil?
90
+ raise ArgumentError, <<-MSG.squish if @connect.blank?
91
+ Unable to generate sub query without setting up a proper way to connect it
92
+ with the main query. Please provide a `connect` property on the "#{table_name}"
93
+ settings.
94
+ MSG
95
+
96
+ left, right = @connect.map(&:to_s)
97
+ condition = @query.arel_table[right].eq(table[left])
98
+
99
+ if @query.where_values_hash.key?(right)
100
+ @sub_query = @query.unscope(where: right.to_sym).where(condition)
101
+ else
102
+ @sub_query = @query.where(condition)
103
+ @query = @query.where(right => nil)
104
+ end
105
+ elsif @sub_query.respond_to?(:call)
106
+ # Call a proc to get the real sub query
107
+ call_args = @sub_query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
108
+ @sub_query = @sub_query.call(*call_args)
109
+ end
110
+ end
111
+
112
+ # Add depth and path if they were defined in settings
113
+ def extra_columns(base, columns, sub_columns)
114
+ return if @query.is_a?(String) || @sub_query.is_a?(String)
115
+
116
+ # Add the connect attribute to the query
117
+ if defined?(@connect)
118
+ columns.unshift(@query.arel_table[@connect[0]])
119
+ sub_columns.unshift(@sub_query.arel_table[@connect[0]])
120
+ end
121
+
122
+ # Build a column to represent the depth of the recursion
123
+ if @depth.present?
124
+ name, start, as = @depth
125
+ col = table[name]
126
+ base.select_extra_values += [col.as(as)] unless as.nil?
127
+
128
+ columns << ::Arel.sql(start.to_s).as(name)
129
+ sub_columns << (col + ::Arel.sql('1')).as(name)
130
+ end
131
+
132
+ # Build a column to represent the path of the record access
133
+ if @path.present?
134
+ name, source, as = @path
135
+ source = @query.arel_table[source || @connect[0]]
136
+
137
+ col = table[name]
138
+ base.select_extra_values += [col.as(as)] unless as.nil?
139
+ parts = [col, source.cast(:varchar)]
140
+
141
+ columns << ::Arel.array([source]).cast(:varchar, true).as(name)
142
+ sub_columns << ::Arel::Nodes::NamedFunction.new('array_append', parts).as(name)
143
+ end
144
+ end
145
+
146
+ end
147
+ end
148
+ end
149
+ end
@@ -4,9 +4,9 @@ module Torque
4
4
  module PostgreSQL
5
5
  class AuxiliaryStatement
6
6
  class Settings < Collector.new(:attributes, :join, :join_type, :query, :requires,
7
- :polymorphic, :through)
7
+ :polymorphic, :through, :union_all, :connect)
8
8
 
9
- attr_reader :base, :source
9
+ attr_reader :base, :source, :depth, :path
10
10
  alias_method :select, :attributes
11
11
  alias_method :cte, :source
12
12
 
@@ -14,9 +14,10 @@ module Torque
14
14
  delegate :table, :table_name, to: :@source
15
15
  delegate :sql, to: ::Arel
16
16
 
17
- def initialize(base, source)
17
+ def initialize(base, source, recursive = false)
18
18
  @base = base
19
19
  @source = source
20
+ @recursive = recursive
20
21
  end
21
22
 
22
23
  def base_name
@@ -27,6 +28,39 @@ module Torque
27
28
  @base.arel_table
28
29
  end
29
30
 
31
+
32
+ def recursive?
33
+ @recursive
34
+ end
35
+
36
+ def depth?
37
+ defined?(@depth)
38
+ end
39
+
40
+ def path?
41
+ defined?(@path)
42
+ end
43
+
44
+ # Add an attribute to the result showing the depth of each iteration
45
+ def with_depth(name = 'depth', start: 0, as: nil)
46
+ @depth = [name.to_s, start, as&.to_s] if recursive?
47
+ end
48
+
49
+ # Add an attribute to the result showing the path of each record
50
+ def with_path(name = 'path', source: nil, as: nil)
51
+ @path = [name.to_s, source&.to_s, as&.to_s] if recursive?
52
+ end
53
+
54
+ # Set recursive operation to use union all
55
+ def union_all!
56
+ @union_all = true if recursive?
57
+ end
58
+
59
+ # Add both depth and path to the result
60
+ def with_depth_and_path
61
+ with_depth && with_path
62
+ end
63
+
30
64
  # Get the arel version of the table set on the query
31
65
  def query_table
32
66
  raise StandardError, 'The query is not defined yet' if query.nil?
@@ -41,36 +75,55 @@ module Torque
41
75
 
42
76
  alias column col
43
77
 
44
- # There are two ways of setting the query:
78
+ # There are three ways of setting the query:
45
79
  # - A simple relation based on a Model
46
80
  # - A Arel-based select manager
47
- # - A string or a proc that requires the table name as first argument
81
+ # - A string or a proc
48
82
  def query(value = nil, command = nil)
49
83
  return @query if value.nil?
50
- return @query = value if relation_query?(value)
51
84
 
52
- if value.is_a?(::Arel::SelectManager)
53
- @query = value
54
- @query_table = value.source.left.name
55
- return
56
- end
85
+ @query = sanitize_query(value, command)
86
+ end
57
87
 
58
- valid_type = command.respond_to?(:call) || command.is_a?(String)
88
+ # Same as query, but for the second part of the union for recursive cte
89
+ def sub_query(value = nil, command = nil)
90
+ return unless recursive?
91
+ return @sub_query if value.nil?
59
92
 
60
- raise ArgumentError, <<-MSG.squish if command.nil?
61
- To use proc or string as query, you need to provide the table name
62
- as the first argument
63
- MSG
93
+ @sub_query = sanitize_query(value, command)
94
+ end
95
+
96
+ # Assume `parent_` as the other part if provided a Symbol or String
97
+ def connect(value = nil)
98
+ return @connect if value.nil?
64
99
 
65
- raise ArgumentError, <<-MSG.squish unless valid_type
66
- Only relation, string and proc are valid object types for query,
67
- #{command.inspect} given.
68
- MSG
100
+ value = [value.to_sym, :"parent_#{value}"] \
101
+ if value.is_a?(String) || value.is_a?(Symbol)
102
+ value = value.to_a.first if value.is_a?(Hash)
69
103
 
70
- @query = command
71
- @query_table = ::Arel::Table.new(value)
104
+ @connect = value
72
105
  end
73
106
 
107
+ alias connect= connect
108
+
109
+ private
110
+
111
+ # Get the query and table from the params
112
+ def sanitize_query(value, command = nil)
113
+ return value if relation_query?(value)
114
+ return value if value.is_a?(::Arel::SelectManager)
115
+
116
+ command = value if command.nil? # For compatibility purposes
117
+ valid_type = command.respond_to?(:call) || command.is_a?(String)
118
+
119
+ raise ArgumentError, <<-MSG.squish unless valid_type
120
+ Only relation, string and proc are valid object types for query,
121
+ #{command.inspect} given.
122
+ MSG
123
+
124
+ command
125
+ end
126
+
74
127
  end
75
128
  end
76
129
  end