torque-postgresql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +31 -0
  4. data/lib/torque/postgresql/adapter/database_statements.rb +103 -0
  5. data/lib/torque/postgresql/adapter/oid/array.rb +19 -0
  6. data/lib/torque/postgresql/adapter/oid/enum.rb +46 -0
  7. data/lib/torque/postgresql/adapter/oid/interval.rb +94 -0
  8. data/lib/torque/postgresql/adapter/oid.rb +15 -0
  9. data/lib/torque/postgresql/adapter/quoting.rb +23 -0
  10. data/lib/torque/postgresql/adapter/schema_definitions.rb +28 -0
  11. data/lib/torque/postgresql/adapter/schema_dumper.rb +31 -0
  12. data/lib/torque/postgresql/adapter/schema_statements.rb +89 -0
  13. data/lib/torque/postgresql/adapter.rb +29 -0
  14. data/lib/torque/postgresql/attributes/builder/enum.rb +151 -0
  15. data/lib/torque/postgresql/attributes/builder.rb +1 -0
  16. data/lib/torque/postgresql/attributes/enum.rb +231 -0
  17. data/lib/torque/postgresql/attributes/lazy.rb +33 -0
  18. data/lib/torque/postgresql/attributes/type_map.rb +46 -0
  19. data/lib/torque/postgresql/attributes.rb +32 -0
  20. data/lib/torque/postgresql/auxiliary_statement.rb +192 -0
  21. data/lib/torque/postgresql/base.rb +28 -0
  22. data/lib/torque/postgresql/collector.rb +31 -0
  23. data/lib/torque/postgresql/config.rb +50 -0
  24. data/lib/torque/postgresql/migration/command_recorder.rb +31 -0
  25. data/lib/torque/postgresql/migration.rb +1 -0
  26. data/lib/torque/postgresql/relation/auxiliary_statement.rb +65 -0
  27. data/lib/torque/postgresql/relation/distinct_on.rb +43 -0
  28. data/lib/torque/postgresql/relation.rb +62 -0
  29. data/lib/torque/postgresql/schema_dumper.rb +37 -0
  30. data/lib/torque/postgresql/version.rb +5 -0
  31. data/lib/torque/postgresql.rb +18 -0
  32. data/lib/torque-postgresql.rb +1 -0
  33. metadata +236 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d27d24270072d57ca693a53b5fcd29ac72b509e0
4
+ data.tar.gz: cc63789993cf2817a2857d6cf5c6b8d31c38dabb
5
+ SHA512:
6
+ metadata.gz: 8b6bd7dc8af51496eecd1c7c7a33f4d3ce8e885e7918668068b8ecf8e7ac9961c1ee4cfe342ecf46c31ccead863b82fe4c3296e523cffd28a5d4ce4c7499be75
7
+ data.tar.gz: 41141267915f0e8ad1d5b6052cab7cc45c9e2bb37ff4d02fc4e819a4c8cf199ba33ea924884c8732bfd691c354908e330f8390e8836906806bc7e685ce2d7f84
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Carlos Silva
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Torque::Postgresql'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ desc 'Prints a schema dump of the test database'
18
+ task :dump do |t|
19
+ lib = File.expand_path('../lib', __FILE__)
20
+ spec = File.expand_path('../spec', __FILE__)
21
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
22
+ $LOAD_PATH.unshift(spec) unless $LOAD_PATH.include?(spec)
23
+
24
+ require 'byebug'
25
+ require 'spec_helper'
26
+ ActiveRecord::SchemaDumper.dump
27
+ end
28
+
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec)
31
+ task default: :spec
@@ -0,0 +1,103 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module DatabaseStatements
5
+
6
+ EXTENDED_DATABASE_TYPES = %i(enum interval)
7
+
8
+ # Check if a given type is valid.
9
+ def valid_type?(type)
10
+ super || extended_types.include?(type)
11
+ end
12
+
13
+ # Get the list of extended types
14
+ def extended_types
15
+ EXTENDED_DATABASE_TYPES
16
+ end
17
+
18
+ # Returns true if type exists.
19
+ def type_exists?(name)
20
+ user_defined_types.key? name.to_s
21
+ end
22
+ alias data_type_exists? type_exists?
23
+
24
+ # Configure the interval format
25
+ def configure_connection
26
+ super
27
+ execute("SET SESSION IntervalStyle TO 'iso_8601'", 'SCHEMA')
28
+ end
29
+
30
+ # Change some of the types being mapped
31
+ def initialize_type_map(m)
32
+ super
33
+ m.register_type 'interval', OID::Interval.new
34
+ end
35
+
36
+ # Add the composite types to be loaded too.
37
+ def load_additional_types(type_map, oids = nil)
38
+ super
39
+
40
+ filter = "AND a.typelem::integer IN (%s)" % oids.join(", ") if oids
41
+
42
+ query = <<-SQL
43
+ SELECT a.typelem AS oid, t.typname, t.typelem,
44
+ t.typdelim, t.typbasetype, t.typtype
45
+ FROM pg_type t
46
+ INNER JOIN pg_type a ON (a.oid = t.typarray)
47
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
48
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
49
+ AND t.typtype IN ( 'c','e' )
50
+ #{filter}
51
+ AND NOT EXISTS(
52
+ SELECT 1 FROM pg_catalog.pg_type el
53
+ WHERE el.oid = t.typelem AND el.typarray = t.oid
54
+ )
55
+ AND (t.typrelid = 0 OR (
56
+ SELECT c.relkind = 'c' FROM pg_catalog.pg_class c
57
+ WHERE c.oid = t.typrelid
58
+ ))
59
+ SQL
60
+
61
+ execute_and_clear(query, 'SCHEMA', []) do |records|
62
+ records.each do |row|
63
+ typtype = row['typtype']
64
+ type = begin
65
+ case
66
+ when typtype == 'e'.freeze then OID::Enum.create(row)
67
+ end
68
+ end
69
+
70
+ type_map.register_type row['oid'].to_i, type
71
+ end
72
+ end
73
+ end
74
+
75
+ # Gets a list of user defined types.
76
+ # You can even choose the +typcategory+ filter
77
+ def user_defined_types(category = nil)
78
+ category_condition = "AND typtype = '#{category}'" unless category.nil?
79
+ select_all(<<-SQL).rows.to_h
80
+ SELECT t.typname AS name,
81
+ CASE t.typtype
82
+ WHEN 'e' THEN 'enum'
83
+ END AS type
84
+ FROM pg_type t
85
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
86
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
87
+ #{category_condition}
88
+ AND NOT EXISTS(
89
+ SELECT 1 FROM pg_catalog.pg_type el
90
+ WHERE el.oid = t.typelem AND el.typarray = t.oid
91
+ )
92
+ AND (t.typrelid = 0 OR (
93
+ SELECT c.relkind = 'c' FROM pg_catalog.pg_class c
94
+ WHERE c.oid = t.typrelid
95
+ ))
96
+ ORDER BY t.typtype DESC
97
+ SQL
98
+ end
99
+
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,19 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module OID
5
+ module Array
6
+
7
+ def initialize(subtype, delimiter = ',')
8
+ super
9
+ @pg_encoder = Coder
10
+ @pg_decoder = Coder
11
+ end
12
+
13
+ end
14
+
15
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.prepend Array
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,46 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module OID
5
+ class Enum < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Enum
6
+
7
+ attr_reader :name, :klass
8
+
9
+ def self.create(row)
10
+ new(row['typname'])
11
+ end
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ @klass = Attributes::Enum.lookup(name)
16
+ end
17
+
18
+ def hash
19
+ [self.class, name].hash
20
+ end
21
+
22
+ def serialize(value)
23
+ return if value.blank?
24
+ value = cast_value(value)
25
+ value.to_s unless value.nil?
26
+ end
27
+
28
+ def assert_valid_value(value)
29
+ cast_value(value)
30
+ end
31
+
32
+ private
33
+
34
+ def cast_value(value)
35
+ return if value.blank?
36
+ return value if value.is_a?(@klass)
37
+ @klass.new(value)
38
+ rescue Attributes::Enum::EnumError
39
+ nil
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,94 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module OID
5
+ class Interval < ActiveModel::Type::Value
6
+
7
+ CAST_PARTS = [:years, :months, :days, :hours, :minutes, :seconds]
8
+
9
+ def type
10
+ :interval
11
+ end
12
+
13
+ # Accepts database-style string, numeric as seconds, array of parts
14
+ # padded to left, or a hash
15
+ #
16
+ # Examples:
17
+ # [12, 0, 0]
18
+ # produces: 12 hours, 0 minutes, and 0 seconds
19
+ #
20
+ # [nil, nil, 3, 0, 0, 0]
21
+ # produces: 3 days, 0 hours, 0 minutes, and 0 seconds
22
+ #
23
+ # {minutes: 12, seconds: 0}
24
+ # produces: 12 minutes, and 0 seconds
25
+ def cast(value)
26
+ return if value.blank?
27
+ case value
28
+ when ::String then deserialize(value)
29
+ when ::ActiveSupport::Duration then value
30
+ when ::Numeric
31
+ parts = CAST_PARTS.map do |part|
32
+ rest, value = value.divmod(1.send(part))
33
+ rest == 0 ? nil : [part, rest]
34
+ end
35
+ parts_to_duration(parts.compact)
36
+ when ::Array
37
+ value.compact!
38
+ parts = CAST_PARTS.drop(6 - value.size).zip(value).to_h
39
+ parts_to_duration(parts)
40
+ when ::Hash
41
+ parts_to_duration(value)
42
+ else
43
+ value
44
+ end
45
+ end
46
+
47
+ # Uses the ActiveSupport::Duration::ISO8601Parser
48
+ # See ActiveSupport::Duration#parse
49
+ # The value must be Integer when no precision is given
50
+ def deserialize(value)
51
+ return if value.blank?
52
+ parts = ActiveSupport::Duration::ISO8601Parser.new(value).parse!
53
+ parts_to_duration(parts)
54
+ end
55
+
56
+ # Uses the ActiveSupport::Duration::ISO8601Serializer
57
+ # See ActiveSupport::Duration#iso8601
58
+ def serialize(value)
59
+ return if value.blank?
60
+ value = cast(value) unless value.is_a?(ActiveSupport::Duration)
61
+ value.iso8601(precision: @scale)
62
+ end
63
+
64
+ # Always use the numeric value for schema dumper
65
+ def type_cast_for_schema(value)
66
+ cast(value).value.inspect
67
+ end
68
+
69
+ # Check if the user input has the correct format
70
+ def assert_valid_value(value)
71
+ # TODO: Implement!
72
+ end
73
+
74
+ # Transform a list of parts into a duration object
75
+ def parts_to_duration(parts)
76
+ parts = parts.to_h.with_indifferent_access.slice(*CAST_PARTS)
77
+ return 0.seconds if parts.blank?
78
+
79
+ seconds = 0
80
+ parts = parts.map do |part, num|
81
+ num = num.to_i unless num.is_a?(Numeric)
82
+ if num > 0
83
+ seconds += num.send(part).value
84
+ [part, num]
85
+ end
86
+ end
87
+ ActiveSupport::Duration.new(seconds, parts.compact)
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'oid/array'
2
+ require_relative 'oid/enum'
3
+ require_relative 'oid/interval'
4
+
5
+ module Torque
6
+ module PostgreSQL
7
+ module Adapter
8
+ module OID
9
+ end
10
+
11
+ ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
12
+ ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgresql)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module Quoting
5
+
6
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
7
+
8
+ # Quotes type names for use in SQL queries.
9
+ def quote_type_name(string, schema = nil)
10
+ name_schema, table = string.to_s.scan(/[^".\s]+|"[^"]*"/)
11
+ if table.nil?
12
+ table = name_schema
13
+ name_schema = nil
14
+ end
15
+
16
+ schema = schema || name_schema || 'public'
17
+ Name.new(schema, table).quoted
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module ColumnMethods
5
+
6
+ def interval(*args, **options)
7
+ args.each { |name| column(name, :interval, options) }
8
+ end
9
+
10
+ def enum(*args, **options)
11
+ args.each do |name|
12
+ type = options.fetch(:subtype, name)
13
+ column(name, type, options)
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ module ColumnDefinition
20
+ attr_accessor :subtype
21
+ end
22
+
23
+ ActiveRecord::ConnectionAdapters::PostgreSQL::Table.include ColumnMethods
24
+ ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.include ColumnMethods
25
+ ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnDefinition.include ColumnDefinition
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module ColumnDumper
5
+
6
+ # Adds +:subtype+ as a valid migration key
7
+ def migration_keys
8
+ super + [:subtype]
9
+ end
10
+
11
+ # Adds +:subtype+ option to the default set
12
+ def prepare_column_options(column)
13
+ spec = super
14
+
15
+ if subtype = schema_subtype(column)
16
+ spec[:subtype] = subtype
17
+ end
18
+
19
+ spec
20
+ end
21
+
22
+ private
23
+
24
+ def schema_subtype(column)
25
+ column.sql_type.to_sym.inspect if [:enum].include? column.type
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,89 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module SchemaStatements
5
+
6
+ # Drops a type.
7
+ def drop_type(name, options = {})
8
+ force = options.fetch(:force, '').upcase
9
+ check = 'IF EXISTS' if options.fetch(:check, true)
10
+ execute <<-SQL
11
+ DROP TYPE #{check}
12
+ #{quote_type_name(name, options[:schema])} #{force}
13
+ SQL
14
+ end
15
+
16
+ # Renames a type.
17
+ def rename_type(type_name, new_name)
18
+ execute <<-SQL
19
+ ALTER TYPE #{quote_type_name(type_name)}
20
+ RENAME TO #{Quoting::Name.new(nil, new_name.to_s).quoted}
21
+ SQL
22
+ end
23
+
24
+ # Creates a new PostgreSQL enumerator type
25
+ #
26
+ # Example:
27
+ # create_enum 'status', ['foo', 'bar']
28
+ # create_enum 'status', ['foo', 'bar'], prefix: true
29
+ # create_enum 'status', ['foo', 'bar'], suffix: 'test'
30
+ # create_enum 'status', ['foo', 'bar'], force: true
31
+ def create_enum(name, values, options = {})
32
+ drop_type(name, options) if options[:force]
33
+ execute <<-SQL
34
+ CREATE TYPE #{quote_type_name(name, options[:schema])} AS ENUM
35
+ (#{quote_enum_values(name, values, options).join(', ')})
36
+ SQL
37
+ end
38
+
39
+ # Changes the enumerator by adding new values
40
+ #
41
+ # Example:
42
+ # add_enum_values 'status', ['baz']
43
+ # add_enum_values 'status', ['baz'], before: 'bar'
44
+ # add_enum_values 'status', ['baz'], after: 'foo'
45
+ # add_enum_values 'status', ['baz'], prepend: true
46
+ def add_enum_values(name, values, options = {})
47
+ before = options.fetch(:before, false)
48
+ after = options.fetch(:after, false)
49
+
50
+ before = enum_values(name).first if options.key? :prepend
51
+ before = quote(before) unless before == false
52
+ after = quote(after) unless after == false
53
+
54
+ quote_enum_values(name, values, options).each do |value|
55
+ reference = "BEFORE #{before}" unless before == false
56
+ reference = "AFTER #{after}" unless after == false
57
+ execute <<-SQL
58
+ ALTER TYPE #{quote_type_name(name, options[:schema])}
59
+ ADD VALUE #{value} #{reference}
60
+ SQL
61
+
62
+ before = false
63
+ after = value
64
+ end
65
+ end
66
+
67
+ # Returns all values that an enum type can have.
68
+ def enum_values(name)
69
+ select_values("SELECT unnest(enum_range(NULL::#{name}))")
70
+ end
71
+
72
+ private
73
+
74
+ def quote_enum_values(name, values, options)
75
+ prefix = options[:prefix]
76
+ prefix = name if prefix === true
77
+
78
+ suffix = options[:suffix]
79
+ suffix = name if suffix === true
80
+
81
+ values.map! do |value|
82
+ quote([prefix, value, suffix].compact.join('_'))
83
+ end
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end