torque-postgresql 3.0.0 → 3.1.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: d4fac19ef8680f477df0f79502331ddda06266658a54c8128321d68145a9f18b
4
- data.tar.gz: 0e93ec49f80d40ec9ce9b7fcc5ef0eff65a882cad79a63e5429657ad9c66691b
3
+ metadata.gz: 25fb390d67c43461cf2c03096990149d2dbcae04072e96d69896d60f8e368863
4
+ data.tar.gz: e2910f192602059a71dbf3cfca2cc412e2d34eb1faacc51620058ce408c1be9f
5
5
  SHA512:
6
- metadata.gz: 4a9abd492b544296c29e0949e1ad0ad4852a1ed0c48379551e5ffbbaad7018579cad994ac2a841231603bdddd8da4b448d7771a9086312289f818124d842d6a6
7
- data.tar.gz: 8019255d20eff471ed177078e1bd6042a0a9b95752d32578bdb8ce86afdf9cffbefa8ed181caab0143f0dbbf80cfc39c99fd005cfc2b28089687b26a635268ef
6
+ metadata.gz: c664a0e98b7cc78b5930d4e3273330caa49cb8a18b792892cc42dae96b2392ebc61b71043c758c5e0abc3c54b12f8ee567fc53a48ad9f6765726ed1e000e2770
7
+ data.tar.gz: cfdb00e3740a05f2cabc1ccb25070ce05f5a628faa84209a899d5a247c17465dba0cac0a09a305e0c19fb05fb456c8d87e05107c9f0acb66803226a86ae2083b
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
@@ -124,18 +155,41 @@ module Torque
124
155
  # Get the list of inherited tables associated with their parent tables
125
156
  def inherited_tables
126
157
  tables = query(<<-SQL, 'SCHEMA')
127
- SELECT child.relname AS table_name,
128
- array_agg(parent.relname) AS inheritances
158
+ SELECT inhrelid::regclass AS table_name,
159
+ inhparent::regclass AS inheritances
129
160
  FROM pg_inherits
130
161
  JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
131
162
  JOIN pg_class child ON pg_inherits.inhrelid = child.oid
132
- GROUP BY child.relname, pg_inherits.inhrelid
133
- ORDER BY pg_inherits.inhrelid
163
+ ORDER BY inhrelid
134
164
  SQL
135
165
 
136
- tables.map do |(table, refs)|
137
- [table, PG::TextDecoder::Array.new.decode(refs)]
138
- end.to_h
166
+ tables.each_with_object({}) do |(child, parent), result|
167
+ (result[child] ||= []) << parent
168
+ end
169
+ end
170
+
171
+ # Get the list of schemas that were created by the user
172
+ def user_defined_schemas
173
+ query_values(user_defined_schemas_sql, 'SCHEMA')
174
+ end
175
+
176
+ # Build the query for allowed schemas
177
+ def user_defined_schemas_sql
178
+ conditions = []
179
+ conditions << <<-SQL if schemas_blacklist.any?
180
+ nspname NOT LIKE ANY (ARRAY['#{schemas_blacklist.join("', '")}'])
181
+ SQL
182
+
183
+ conditions << <<-SQL if schemas_whitelist.any?
184
+ nspname LIKE ANY (ARRAY['#{schemas_whitelist.join("', '")}'])
185
+ SQL
186
+
187
+ <<-SQL.squish
188
+ SELECT nspname
189
+ FROM pg_catalog.pg_namespace
190
+ WHERE 1=1 AND #{conditions.join(' AND ')}
191
+ ORDER BY oid
192
+ SQL
139
193
  end
140
194
 
141
195
  # Get the list of columns, and their definition, but only from the
@@ -12,6 +12,11 @@ module Torque
12
12
  stream
13
13
  end
14
14
 
15
+ def extensions(stream) # :nodoc:
16
+ super
17
+ user_defined_schemas(stream)
18
+ end
19
+
15
20
  # Translate +:enum_set+ into +:enum+
16
21
  def schema_type(column)
17
22
  column.type == :enum_set ? :enum : super
@@ -20,8 +25,23 @@ module Torque
20
25
  private
21
26
 
22
27
  def tables(stream) # :nodoc:
28
+ around_tables(stream) { dump_tables(stream) }
29
+ end
30
+
31
+ def around_tables(stream)
32
+ functions(stream) if fx_functions_position == :beginning
33
+
34
+ yield
35
+
36
+ functions(stream) if fx_functions_position == :end
37
+ triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
38
+ end
39
+
40
+ def dump_tables(stream)
23
41
  inherited_tables = @connection.inherited_tables
24
- sorted_tables = @connection.tables.sort - @connection.views
42
+ sorted_tables = (@connection.tables - @connection.views).sort_by do |table_name|
43
+ table_name.split(/(?:public)?\./).reverse
44
+ end
25
45
 
26
46
  stream.puts " # These are the common tables"
27
47
  (sorted_tables - inherited_tables.keys).each do |table_name|
@@ -38,7 +58,7 @@ module Torque
38
58
 
39
59
  # Add the inherits setting
40
60
  sub_stream.rewind
41
- inherits.map!(&:to_sym)
61
+ inherits.map! { |parent| parent.to_s.sub(/\Apublic\./, '') }
42
62
  inherits = inherits.first if inherits.size === 1
43
63
  inherits = ", inherits: #{inherits.inspect} do |t|"
44
64
  table_dump = sub_stream.read.gsub(/ do \|t\|$/, inherits)
@@ -55,13 +75,25 @@ module Torque
55
75
  foreign_keys(tbl, stream) unless ignored?(tbl)
56
76
  end
57
77
  end
78
+ end
79
+
80
+ # Make sure to remove the schema from the table name
81
+ def remove_prefix_and_suffix(table)
82
+ super(table.sub(/\A[a-z0-9_]*\./, ''))
83
+ end
58
84
 
59
- # Scenic integration
60
- views(stream) if defined?(::Scenic)
85
+ # Dump user defined schemas
86
+ def user_defined_schemas(stream)
87
+ return if (list = (@connection.user_defined_schemas - ['public'])).empty?
61
88
 
62
- # FX integration
63
- functions(stream) if defined?(::Fx::SchemaDumper::Function)
64
- triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
89
+ stream.puts " # Custom schemas defined in this database."
90
+ list.each { |name| stream.puts " create_schema \"#{name}\", force: :cascade" }
91
+ stream.puts
92
+ end
93
+
94
+ def fx_functions_position
95
+ return unless defined?(::Fx::SchemaDumper::Function)
96
+ Fx.configuration.dump_functions_at_beginning_of_schema ? :beginning : :end
65
97
  end
66
98
  end
67
99
 
@@ -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
@@ -64,12 +79,37 @@ module Torque
64
79
 
65
80
  # Rewrite the method that creates tables to easily accept extra options
66
81
  def create_table(table_name, **options, &block)
82
+ table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
83
+
67
84
  options[:id] = false if options[:inherits].present? &&
68
85
  options[:primary_key].blank? && options[:id].blank?
69
86
 
70
87
  super table_name, **options, &block
71
88
  end
72
89
 
90
+ # Add the schema option when extracting table options
91
+ def table_options(table_name)
92
+ parts = table_name.split('.').reverse
93
+ return super unless parts.size == 2 && parts[1] != 'public'
94
+
95
+ (super || {}).merge(schema: parts[1])
96
+ end
97
+
98
+ # When dumping the schema we need to add all schemas, not only those
99
+ # active for the current +schema_search_path+
100
+ def quoted_scope(name = nil, type: nil)
101
+ return super unless name.nil?
102
+
103
+ super.merge(schema: "ANY ('{#{user_defined_schemas.join(',')}}')")
104
+ end
105
+
106
+ # Fix the query to include the schema on tables names when dumping
107
+ def data_source_sql(name = nil, type: nil)
108
+ return super unless name.nil?
109
+
110
+ super.sub('SELECT c.relname FROM', "SELECT n.nspname || '.' || c.relname FROM")
111
+ end
112
+
73
113
  private
74
114
 
75
115
  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
@@ -6,7 +6,7 @@ module Torque
6
6
  module Preloader
7
7
  module LoaderQuery
8
8
  def foreign_column
9
- @foreign_column ||= scope.columns_hash[association_key_name]
9
+ @foreign_column ||= scope.columns_hash[association_key_name.to_s]
10
10
  end
11
11
 
12
12
  def load_records_for_keys(keys, &block)
@@ -5,15 +5,27 @@ module Torque
5
5
  module Base
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ ##
9
+ # :singleton-method: schema
10
+ # :call-seq: schema
11
+ #
12
+ # The schema to which the table belongs to.
13
+
8
14
  included do
9
15
  mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
16
+ class_attribute :schema, instance_writer: false
10
17
  end
11
18
 
12
19
  module ClassMethods
13
20
  delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
14
21
 
15
- # Wenever it's inherited, add a new list of auxiliary statements
16
- # It also adds an auxiliary statement to load inherited records' relname
22
+ # Make sure that table name is an instance of TableName class
23
+ def reset_table_name
24
+ self.table_name = TableName.new(self, super)
25
+ end
26
+
27
+ # Whenever the base model is inherited, add a list of auxiliary
28
+ # statements like the one that loads inherited records' relname
17
29
  def inherited(subclass)
18
30
  super
19
31
 
@@ -22,32 +34,14 @@ module Torque
22
34
 
23
35
  record_class = ActiveRecord::Relation._record_class_attribute
24
36
 
25
- # Define helper methods to return the class of the given records
26
- subclass.auxiliary_statement record_class do |cte|
27
- pg_class = ::Arel::Table.new('pg_class')
28
- arel_query = ::Arel::SelectManager.new(pg_class)
29
- arel_query.project(pg_class['oid'], pg_class['relname'].as(record_class.to_s))
30
-
31
- cte.query 'pg_class', arel_query.to_sql
32
- cte.attributes col(record_class) => record_class
33
- cte.join tableoid: :oid
34
- end
35
-
36
37
  # Define the dynamic attribute that returns the same information as
37
38
  # the one provided by the auxiliary statement
38
39
  subclass.dynamic_attribute(record_class) do
39
- next self.class.table_name unless self.class.physically_inheritances?
40
-
41
- pg_class = ::Arel::Table.new('pg_class')
42
- source = ::Arel::Table.new(subclass.table_name, as: 'source')
43
- quoted_id = ::Arel::Nodes::Quoted.new(id)
44
-
45
- query = ::Arel::SelectManager.new(pg_class)
46
- query.join(source).on(pg_class['oid'].eq(source['tableoid']))
47
- query.where(source[subclass.primary_key].eq(quoted_id))
48
- query.project(pg_class['relname'])
40
+ klass = self.class
41
+ next klass.table_name unless klass.physically_inheritances?
49
42
 
50
- self.class.connection.select_value(query)
43
+ query = klass.unscoped.where(subclass.primary_key => id)
44
+ query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
51
45
  end
52
46
  end
53
47
 
@@ -39,6 +39,19 @@ module Torque
39
39
 
40
40
  end
41
41
 
42
+ # Configure multiple schemas
43
+ config.nested(:schemas) do |schemas|
44
+
45
+ # Defines a list of LIKE-based schemas to not consider for a multiple
46
+ # schema database
47
+ schemas.blacklist = %w[information_schema pg_%]
48
+
49
+ # Defines a list of LIKE-based schemas to consider for a multiple schema
50
+ # database
51
+ schemas.whitelist = %w[public]
52
+
53
+ end
54
+
42
55
  # Configure auxiliary statement features
43
56
  config.nested(:auxiliary_statement) do |cte|
44
57
 
@@ -5,24 +5,24 @@ module Torque
5
5
  module Migration
6
6
  module CommandRecorder
7
7
 
8
- # Records the rename operation for types.
8
+ # Records the rename operation for types
9
9
  def rename_type(*args, &block)
10
10
  record(:rename_type, args, &block)
11
11
  end
12
12
 
13
- # Inverts the type name.
13
+ # Inverts the type rename operation
14
14
  def invert_rename_type(args)
15
15
  [:rename_type, args.reverse]
16
16
  end
17
17
 
18
- # Records the creation of the enum to be reverted.
19
- def create_enum(*args, &block)
20
- record(:create_enum, args, &block)
18
+ # Records the creation of a schema
19
+ def create_schema(*args, &block)
20
+ record(:create_schema, args, &block)
21
21
  end
22
22
 
23
- # Inverts the creation of the enum.
24
- def invert_create_enum(args)
25
- [:drop_type, [args.first]]
23
+ # Inverts the creation of a schema
24
+ def invert_create_schema(args)
25
+ [:drop_schema, [args.first]]
26
26
  end
27
27
 
28
28
  end
@@ -40,34 +40,29 @@ module Torque
40
40
  result
41
41
  end
42
42
 
43
- # Build the id constraint checking if both types are perfect matching
43
+ # Build the id constraint checking if both types are perfect matching.
44
+ # The klass attribute (left side) will always be a column attribute
44
45
  def build_id_constraint(klass_attr, source_attr)
45
46
  return klass_attr.eq(source_attr) unless connected_through_array?
46
47
 
47
48
  # Klass and key are associated with the reflection Class
48
- klass_type = klass.columns_hash[join_primary_key.to_s]
49
- # active_record and foreign_key are associated with the source Class
50
- source_type = active_record.columns_hash[join_foreign_key.to_s]
51
-
52
- # If both are attributes but the left side is not an array, and the
53
- # right side is, use the ANY operation
54
- any_operation = arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
55
- return klass_attr.eq(any_operation) if any_operation
49
+ klass_type = klass.columns_hash[join_keys.key.to_s]
50
+
51
+ # Apply an ANY operation which checks if the single value on the left
52
+ # side exists in the array on the right side
53
+ if source_attr.is_a?(AREL_ATTR)
54
+ any_value = [klass_attr, source_attr]
55
+ any_value.reverse! if klass_type.try(:array?)
56
+ return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
57
+ end
56
58
 
57
59
  # If the left side is not an array, just use the IN condition
58
60
  return klass_attr.in(source_attr) unless klass_type.try(:array)
59
61
 
60
- # Decide if should apply a cast to ensure same type comparision
61
- should_cast = klass_type.type.eql?(:integer) && source_type.type.eql?(:integer)
62
- should_cast &= !klass_type.sql_type.eql?(source_type.sql_type)
63
- should_cast |= !(klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR))
64
-
65
- # Apply necessary transformations to values
66
- klass_attr = cast_constraint_to_array(klass_type, klass_attr, should_cast)
67
- source_attr = cast_constraint_to_array(source_type, source_attr, should_cast)
68
-
69
- # Return the overlap condition
70
- klass_attr.overlaps(source_attr)
62
+ # Build the overlap condition (array && array) ensuring that the right
63
+ # side has the same type as the left side
64
+ source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
65
+ klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
71
66
  end
72
67
 
73
68
  # TODO: Deprecate this method
@@ -83,24 +78,6 @@ module Torque
83
78
 
84
79
  build_id_constraint(klass_attr, source_attr)
85
80
  end
86
-
87
- # Prepare a value for an array constraint overlap condition
88
- def cast_constraint_to_array(type, value, should_cast)
89
- base_ready = type.try(:array) && value.is_a?(AREL_ATTR)
90
- return value if base_ready && (type.sql_type.eql?(ARR_NO_CAST) || !should_cast)
91
-
92
- value = ::Arel::Nodes.build_quoted(Array.wrap(value)) unless base_ready
93
- value = value.cast(ARR_CAST) if should_cast
94
- value
95
- end
96
-
97
- # Check if it's possible to turn both attributes into an ANY condition
98
- def arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
99
- return unless !klass_type.try(:array) && source_type.try(:array) &&
100
- klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR)
101
-
102
- ::Arel::Nodes::NamedFunction.new('ANY', [source_attr])
103
- end
104
81
  end
105
82
 
106
83
  ::ActiveRecord::Reflection::AbstractReflection.prepend(AbstractReflection)
@@ -109,7 +109,6 @@ module Torque
109
109
 
110
110
  # Try to find a model based on a given table
111
111
  def lookup_model(table_name, scoped_class = '')
112
- # byebug if table_name == 'activities'
113
112
  scoped_class = scoped_class.name if scoped_class.is_a?(Class)
114
113
  return @data_sources_model_names[table_name] \
115
114
  if @data_sources_model_names.key?(table_name)
@@ -118,6 +117,12 @@ module Torque
118
117
  scopes = scoped_class.scan(/(?:::)?[A-Z][a-z]+/)
119
118
  scopes.unshift('Object::')
120
119
 
120
+ # Check if the table name comes with a schema
121
+ if table_name.include?('.')
122
+ schema, table_name = table_name.split('.')
123
+ scopes.insert(1, schema.camelize) if schema != 'public'
124
+ end
125
+
121
126
  # Consider the maximum namespaced possible model name
122
127
  max_name = table_name.tr('_', '/').camelize.split(/(::)/)
123
128
  max_name[-1] = max_name[-1].singularize
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ class TableName < Delegator
6
+ def initialize(klass, table_name)
7
+ @klass = klass
8
+ @table_name = table_name
9
+ end
10
+
11
+ def schema
12
+ return @schema if defined?(@schema)
13
+
14
+ @schema = ([@klass] + @klass.module_parents[0..-2]).find do |klass|
15
+ next unless klass.respond_to?(:schema)
16
+ break klass.schema
17
+ end
18
+ end
19
+
20
+ def to_s
21
+ schema.nil? ? @table_name : "#{schema}.#{@table_name}"
22
+ end
23
+
24
+ alias __getobj__ to_s
25
+
26
+ def ==(other)
27
+ other.to_s =~ /("?#{schema | search_path_schemes.join('|')}"?\.)?"?#{@table_name}"?/
28
+ end
29
+
30
+ def __setobj__(value)
31
+ @table_name = value
32
+ end
33
+
34
+ private
35
+
36
+ def search_path_schemes
37
+ klass.connection.schemas_search_path_sanitized
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '3.0.0'
5
+ VERSION = '3.1.0'
6
6
  end
7
7
  end
@@ -20,12 +20,13 @@ require 'torque/postgresql/associations'
20
20
  require 'torque/postgresql/attributes'
21
21
  require 'torque/postgresql/autosave_association'
22
22
  require 'torque/postgresql/auxiliary_statement'
23
- require 'torque/postgresql/base'
24
23
  require 'torque/postgresql/inheritance'
24
+ require 'torque/postgresql/base' # Needs to be after inheritance
25
25
  require 'torque/postgresql/insert_all'
26
26
  require 'torque/postgresql/migration'
27
27
  require 'torque/postgresql/relation'
28
28
  require 'torque/postgresql/reflection'
29
29
  require 'torque/postgresql/schema_cache'
30
+ require 'torque/postgresql/table_name'
30
31
 
31
32
  require 'torque/postgresql/railtie' if defined?(Rails)
@@ -0,0 +1,5 @@
1
+ module Internal
2
+ class User < ActiveRecord::Base
3
+ self.schema = 'internal'
4
+ end
5
+ end
data/spec/schema.rb CHANGED
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- version = 2
13
+ version = 1
14
14
 
15
15
  return if ActiveRecord::Migrator.current_version == version
16
16
  ActiveRecord::Schema.define(version: version) do
@@ -20,6 +20,9 @@ ActiveRecord::Schema.define(version: version) do
20
20
  enable_extension "pgcrypto"
21
21
  enable_extension "plpgsql"
22
22
 
23
+ # Custom schemas used in this database.
24
+ create_schema "internal", force: :cascade
25
+
23
26
  # Custom types defined in this database.
24
27
  # Note that some types may not work with other database engines. Be careful if changing database.
25
28
  create_enum "content_status", ["created", "draft", "published", "archived"]
@@ -117,6 +120,13 @@ ActiveRecord::Schema.define(version: version) do
117
120
  t.datetime "updated_at", null: false
118
121
  end
119
122
 
123
+ create_table "users", schema: "internal", force: :cascade do |t|
124
+ t.string "email"
125
+ t.datetime "created_at", null: false
126
+ t.datetime "updated_at", null: false
127
+ t.index ["email"], name: "index_internal_users_on_email", unique: true
128
+ end
129
+
120
130
  create_table "activities", force: :cascade do |t|
121
131
  t.integer "author_id"
122
132
  t.string "title"
data/spec/spec_helper.rb CHANGED
@@ -22,7 +22,7 @@ cleaner = ->() do
22
22
  end
23
23
 
24
24
  load File.join('schema.rb')
25
- Dir.glob(File.join('spec', '{models,factories,mocks}', '*.rb')) do |file|
25
+ Dir.glob(File.join('spec', '{models,factories,mocks}', '**', '*.rb')) do |file|
26
26
  require file[5..-4]
27
27
  end
28
28
 
@@ -39,6 +39,8 @@ RSpec.configure do |config|
39
39
 
40
40
  # Handles acton before rspec initialize
41
41
  config.before(:suite) do
42
+ Torque::PostgreSQL.config.schemas.whitelist << 'internal'
43
+ ActiveSupport::Deprecation.silenced = true
42
44
  DatabaseCleaner.clean_with(:truncation)
43
45
  end
44
46
 
@@ -393,4 +393,51 @@ RSpec.describe 'BelongsToMany' do
393
393
  end
394
394
  end
395
395
  end
396
+
397
+ context 'using uuid' do
398
+ let(:connection) { ActiveRecord::Base.connection }
399
+ let(:game) { Class.new(ActiveRecord::Base) }
400
+ let(:player) { Class.new(ActiveRecord::Base) }
401
+ let(:other) { player.create }
402
+
403
+ # TODO: Set as a shred example
404
+ before do
405
+ connection.create_table(:players, id: :uuid) { |t| t.string :name }
406
+ connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
407
+
408
+ game.table_name = 'games'
409
+ player.table_name = 'players'
410
+ game.belongs_to_many :players, anonymous_class: player,
411
+ inverse_of: false, foreign_key: :player_ids
412
+ end
413
+
414
+ subject { game.create }
415
+
416
+ it 'loads associated records' do
417
+ subject.update(player_ids: [other.id])
418
+ expect(subject.players.to_sql).to be_eql(<<-SQL.squish)
419
+ SELECT "players".* FROM "players" WHERE "players"."id" IN ('#{other.id}')
420
+ SQL
421
+
422
+ expect(subject.players.load).to be_a(ActiveRecord::Associations::CollectionProxy)
423
+ expect(subject.players.to_a).to be_eql([other])
424
+ end
425
+
426
+ it 'can preload records' do
427
+ records = 5.times.map { player.create }
428
+ subject.players.concat(records)
429
+
430
+ entries = game.all.includes(:players).load
431
+
432
+ expect(entries.size).to be_eql(1)
433
+ expect(entries.first.players).to be_loaded
434
+ expect(entries.first.players.size).to be_eql(5)
435
+ end
436
+
437
+ it 'can joins records' do
438
+ query = game.all.joins(:players)
439
+ expect(query.to_sql).to match(/INNER JOIN "players"/)
440
+ expect { query.load }.not_to raise_error
441
+ end
442
+ end
396
443
  end
@@ -43,18 +43,19 @@ RSpec.describe 'Enum' do
43
43
  end
44
44
 
45
45
  context 'on schema' do
46
+ let(:dump_result) do
47
+ ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
48
+ dump_result.string
49
+ end
50
+
46
51
  it 'can be used on tables' do
47
- dump_io = StringIO.new
48
52
  checker = /t\.enum +"conflicts", +array: true, +enum_type: "conflicts"/
49
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
50
- expect(dump_io.string).to match checker
53
+ expect(dump_result).to match checker
51
54
  end
52
55
 
53
56
  xit 'can have a default value as an array of symbols' do
54
- dump_io = StringIO.new
55
57
  checker = /t\.enum +"types", +default: \[:A, :B\], +array: true, +enum_type: "types"/
56
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
57
- expect(dump_io.string).to match checker
58
+ expect(dump_result).to match checker
58
59
  end
59
60
  end
60
61
 
@@ -411,4 +411,48 @@ RSpec.describe 'HasMany' do
411
411
  expect { query.load }.not_to raise_error
412
412
  end
413
413
  end
414
+
415
+ context 'using uuid' do
416
+ let(:connection) { ActiveRecord::Base.connection }
417
+ let(:game) { Class.new(ActiveRecord::Base) }
418
+ let(:player) { Class.new(ActiveRecord::Base) }
419
+
420
+ # TODO: Set as a shred example
421
+ before do
422
+ connection.create_table(:players, id: :uuid) { |t| t.string :name }
423
+ connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
424
+
425
+ game.table_name = 'games'
426
+ player.table_name = 'players'
427
+ player.has_many :games, array: true, anonymous_class: game,
428
+ inverse_of: false, foreign_key: :player_ids
429
+ end
430
+
431
+ subject { player.create }
432
+
433
+ it 'loads associated records' do
434
+ expect(subject.games.to_sql).to match(Regexp.new(<<-SQL.squish))
435
+ SELECT "games"\\.\\* FROM "games"
436
+ WHERE \\(?"games"\\."player_ids" && ARRAY\\['#{subject.id}'\\]::uuid\\[\\]\\)?
437
+ SQL
438
+
439
+ expect(subject.games.load).to be_a(ActiveRecord::Associations::CollectionProxy)
440
+ expect(subject.games.to_a).to be_eql([])
441
+ end
442
+
443
+ it 'can preload records' do
444
+ 5.times { game.create(player_ids: [subject.id]) }
445
+ entries = player.all.includes(:games).load
446
+
447
+ expect(entries.size).to be_eql(1)
448
+ expect(entries.first.games).to be_loaded
449
+ expect(entries.first.games.size).to be_eql(5)
450
+ end
451
+
452
+ it 'can joins records' do
453
+ query = player.all.joins(:games)
454
+ expect(query.to_sql).to match(/INNER JOIN "games"/)
455
+ expect { query.load }.not_to raise_error
456
+ end
457
+ end
414
458
  end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Schema' do
4
+ let(:connection) { ActiveRecord::Base.connection }
5
+
6
+ before do
7
+ connection.instance_variable_set(:@schmeas_blacklist, nil)
8
+ connection.instance_variable_set(:@schmeas_whitelist, nil)
9
+ end
10
+
11
+ context 'on migration' do
12
+ it 'can check for existance' do
13
+ expect(connection.schema_exists?(:information_schema)).to be_falsey
14
+ expect(connection.schema_exists?(:information_schema, filtered: false)).to be_truthy
15
+ end
16
+
17
+ it 'can be created' do
18
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
19
+ connection.create_schema(:legacy)
20
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
21
+ end
22
+
23
+ it 'can be deleted' do
24
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
25
+
26
+ connection.create_schema(:legacy)
27
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
28
+
29
+ connection.drop_schema(:legacy)
30
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
31
+ end
32
+
33
+ it 'works with whitelist' do
34
+ expect(connection.schema_exists?(:legacy)).to be_falsey
35
+ connection.create_schema(:legacy)
36
+
37
+ expect(connection.schema_exists?(:legacy)).to be_falsey
38
+ expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
39
+
40
+ connection.schemas_whitelist.push('legacy')
41
+ expect(connection.schema_exists?(:legacy)).to be_truthy
42
+ end
43
+ end
44
+
45
+ context 'on schema' do
46
+ let(:dump_result) do
47
+ ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
48
+ dump_result.string
49
+ end
50
+
51
+ it 'does not add when there is no extra schemas' do
52
+ connection.drop_schema(:internal, force: :cascade)
53
+ expect(dump_result).not_to match /Custom schemas defined in this database/
54
+ end
55
+
56
+ it 'does not include tables from blacklisted schemas' do
57
+ connection.schemas_blacklist.push('internal')
58
+ expect(dump_result).not_to match /create_table \"users\",.*schema: +"internal"/
59
+ end
60
+
61
+ context 'with internal schema whitelisted' do
62
+ before { connection.schemas_whitelist.push('internal') }
63
+
64
+ it 'dumps the schemas' do
65
+ expect(dump_result).to match /create_schema \"internal\"/
66
+ end
67
+
68
+ it 'shows the internal users table in the connection tables list' do
69
+ expect(connection.tables).to include('internal.users')
70
+ end
71
+
72
+ it 'dumps tables on whitelisted schemas' do
73
+ expect(dump_result).to match /create_table \"users\",.*schema: +"internal"/
74
+ end
75
+ end
76
+ end
77
+
78
+ context 'on relation' do
79
+ let(:model) { Internal::User }
80
+
81
+ it 'adds the schema to the query' do
82
+ expect(model.all.to_sql).to match(/FROM "internal"."users"/)
83
+ end
84
+
85
+ it 'can load the schema from the module' do
86
+ allow(Internal).to receive(:schema).and_return('internal')
87
+ allow(model).to receive(:schema).and_return(nil)
88
+
89
+ expect(model.all.to_sql).to match(/FROM "internal"."users"/)
90
+ end
91
+ end
92
+ end
@@ -73,37 +73,33 @@ RSpec.describe 'TableInheritance' do
73
73
  end
74
74
 
75
75
  context 'on schema' do
76
- it 'dumps single inheritance with body' do
77
- dump_io = StringIO.new
78
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
76
+ let(:dump_result) do
77
+ ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
78
+ dump_result.string
79
+ end
79
80
 
81
+ it 'dumps single inheritance with body' do
80
82
  parts = '"activity_books"'
81
83
  parts << ', id: false'
82
84
  parts << ', force: :cascade'
83
- parts << ', inherits: :activities'
84
- expect(dump_io.string).to match(/create_table #{parts} do /)
85
+ parts << ', inherits: "activities"'
86
+ expect(dump_result).to match(/create_table #{parts} do /)
85
87
  end
86
88
 
87
89
  it 'dumps single inheritance without body' do
88
- dump_io = StringIO.new
89
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
90
-
91
90
  parts = '"activity_post_samples"'
92
91
  parts << ', id: false'
93
92
  parts << ', force: :cascade'
94
- parts << ', inherits: :activity_posts'
95
- expect(dump_io.string).to match(/create_table #{parts}(?! do \|t\|)/)
93
+ parts << ', inherits: "activity_posts"'
94
+ expect(dump_result).to match(/create_table #{parts}(?! do \|t\|)/)
96
95
  end
97
96
 
98
97
  it 'dumps multiple inheritance' do
99
- dump_io = StringIO.new
100
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
101
-
102
98
  parts = '"activity_posts"'
103
99
  parts << ', id: false'
104
100
  parts << ', force: :cascade'
105
- parts << ', inherits: (\[:images, :activities\]|\[:activities, :images\])'
106
- expect(dump_io.string).to match(/create_table #{parts}/)
101
+ parts << ', inherits: (\["images", "activities"\]|\["activities", "images"\])'
102
+ expect(dump_result).to match(/create_table #{parts}/)
107
103
  end
108
104
  end
109
105
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: torque-postgresql
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-04 00:00:00.000000000 Z
11
+ date: 2022-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -155,7 +155,7 @@ dependencies:
155
155
  description: Add support to complex resources of PostgreSQL, like data types, array
156
156
  associations, and auxiliary statements (CTE)
157
157
  email:
158
- - carlinhus.fsilva@gmail.com
158
+ - me@carlosfsilva.com
159
159
  executables: []
160
160
  extensions: []
161
161
  extra_rdoc_files: []
@@ -232,6 +232,7 @@ files:
232
232
  - lib/torque/postgresql/relation/inheritance.rb
233
233
  - lib/torque/postgresql/relation/merger.rb
234
234
  - lib/torque/postgresql/schema_cache.rb
235
+ - lib/torque/postgresql/table_name.rb
235
236
  - lib/torque/postgresql/version.rb
236
237
  - spec/en.yml
237
238
  - spec/factories/authors.rb
@@ -254,6 +255,7 @@ files:
254
255
  - spec/models/course.rb
255
256
  - spec/models/geometry.rb
256
257
  - spec/models/guest_comment.rb
258
+ - spec/models/internal/user.rb
257
259
  - spec/models/item.rb
258
260
  - spec/models/post.rb
259
261
  - spec/models/question.rb
@@ -280,13 +282,18 @@ files:
280
282
  - spec/tests/period_spec.rb
281
283
  - spec/tests/quoting_spec.rb
282
284
  - spec/tests/relation_spec.rb
285
+ - spec/tests/schema_spec.rb
283
286
  - spec/tests/table_inheritance_spec.rb
284
287
  homepage: https://github.com/crashtech/torque-postgresql
285
288
  licenses:
286
289
  - MIT
287
- metadata: {}
290
+ metadata:
291
+ source_code_uri: https://github.com/crashtech/torque-postgresql
292
+ bug_tracker_uri: https://github.com/crashtech/torque-postgresql/issues
288
293
  post_install_message:
289
- rdoc_options: []
294
+ rdoc_options:
295
+ - "--title"
296
+ - Torque PostgreSQL
290
297
  require_paths:
291
298
  - lib
292
299
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -326,6 +333,7 @@ test_files:
326
333
  - spec/models/course.rb
327
334
  - spec/models/geometry.rb
328
335
  - spec/models/guest_comment.rb
336
+ - spec/models/internal/user.rb
329
337
  - spec/models/item.rb
330
338
  - spec/models/post.rb
331
339
  - spec/models/question.rb
@@ -352,4 +360,5 @@ test_files:
352
360
  - spec/tests/period_spec.rb
353
361
  - spec/tests/quoting_spec.rb
354
362
  - spec/tests/relation_spec.rb
363
+ - spec/tests/schema_spec.rb
355
364
  - spec/tests/table_inheritance_spec.rb