sequel-pg-comment 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ #:nodoc:
2
+ # Enhancements to the standard schema modification methods in a
3
+ # block-form `create_table` method, to support setting comments via the
4
+ # `:comment` option.
5
+ #
6
+ module Sequel::Extension::PgComment::CreateTableGeneratorMethods
7
+ # An array of all the comments that this generator has seen fit to
8
+ # create.
9
+ #
10
+ # @return [Array<SqlGenerator>]
11
+ #
12
+ attr_reader :comments
13
+
14
+ include Sequel::Extension::PgComment
15
+
16
+ # Enhanced version of the `column` table definition method, which
17
+ # supports setting a comment on the column.
18
+ #
19
+ # @option [String] :comment The comment to set on the column that is
20
+ # being defined.
21
+ #
22
+ def column(*args)
23
+ super
24
+
25
+ if args.last.is_a?(Hash) && args.last[:comment]
26
+ comments << SqlGenerator.create(:column, args.first, args.last[:comment])
27
+ end
28
+ end
29
+
30
+ # Enhanced version of the `primary_key` table definition method, which
31
+ # supports setting a comment on either the column or constraint.
32
+ #
33
+ # If the primary key is composite (`name` is an array), then the comment
34
+ # will be placed on the index. Otherwise, the comment will be set
35
+ # on the column itself.
36
+ #
37
+ # @option [String] :comment The comment to set on the column or
38
+ # index that is being defined.
39
+ #
40
+ def primary_key(name, *args)
41
+ if args.last.is_a?(Hash) && args.last[:comment] and !name.is_a? Array
42
+ # The composite primary key case will be handled by the
43
+ # `composite_primary_key` method, so we don't have to deal with it
44
+ # here.
45
+ comments << SqlGenerator.create(:column, name, args.last[:comment])
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ # Enhanced version of the `composite_primary_key` table definition method,
52
+ # which supports setting a comment on the primary key index.
53
+ #
54
+ # @option [String] :comment The comment to set on the primary key index.
55
+ #
56
+ def composite_primary_key(columns, *args)
57
+ if args.last.is_a?(Hash) and args.last[:comment]
58
+ if args.last[:name]
59
+ comments << SqlGenerator.create(
60
+ :index,
61
+ args.last[:name],
62
+ args.last[:comment]
63
+ )
64
+ else
65
+ comments << PrefixSqlGenerator.new(:index, :_pkey, args.last[:comment])
66
+ end
67
+ end
68
+
69
+ super
70
+ end
71
+
72
+ # Enhanced version of the `composite_foreign_key` table definition method,
73
+ # which supports setting a comment on the FK constraint.
74
+ #
75
+ # @option [String] :comment The comment to set on the foreign key constraint.
76
+ #
77
+ def composite_foreign_key(columns, opts)
78
+ if opts.is_a?(Hash) and opts[:comment] and opts[:table]
79
+ if opts[:name]
80
+ comments << SqlGenerator.create(
81
+ :constraint,
82
+ opts[:name],
83
+ opts[:comment]
84
+ )
85
+ else
86
+ comments << SqlGenerator.create(
87
+ :constraint,
88
+ "#{opts[:table]}_#{columns.first}_fkey".to_sym,
89
+ opts[:comment]
90
+ )
91
+ end
92
+ end
93
+
94
+ super
95
+ end
96
+
97
+ # Enhanced version of the `index` table definition method,
98
+ # which supports setting a comment on the index.
99
+ #
100
+ # @option [String] :comment The comment to set on the index that is being
101
+ # defined.
102
+ #
103
+ def index(columns, opts = OPTS)
104
+ if opts[:comment]
105
+ if opts[:name]
106
+ comments << SqlGenerator.create(:index, opts[:name], opts[:comment])
107
+ else
108
+ comments << PrefixSqlGenerator.new(
109
+ :index,
110
+ ("_" + [columns].flatten.map(&:to_s).join('_') + "_index").to_sym,
111
+ opts[:comment]
112
+ )
113
+ end
114
+ end
115
+
116
+ super
117
+ end
118
+
119
+ # Enhanced version of the `unique` table definition method,
120
+ # which supports setting a comment on the unique index.
121
+ #
122
+ # @option [String] :comment The comment to set on the index that will be
123
+ # defined.
124
+ #
125
+ def unique(columns, opts = OPTS)
126
+ if opts[:comment]
127
+ if opts[:name]
128
+ comments << SqlGenerator.create(:index, opts[:name], opts[:comment])
129
+ else
130
+ comments << PrefixSqlGenerator.new(
131
+ :index,
132
+ ("_" + [columns].flatten.map(&:to_s).join('_') + "_key").to_sym,
133
+ opts[:comment]
134
+ )
135
+ end
136
+ end
137
+
138
+ super
139
+ end
140
+
141
+ # Enhanced version of the `constraint` table definition method,
142
+ # which supports setting a comment on the constraint.
143
+ #
144
+ # @option [String] :comment The comment to set on the constraint that is
145
+ # being defined.
146
+ #
147
+ def constraint(name, *args, &block)
148
+ opts = name.is_a?(Hash) ? name : (args.last.is_a?(Hash) ? args.last : {})
149
+
150
+ if opts[:comment]
151
+ if name
152
+ comments << SqlGenerator.create(:constraint, name, opts[:comment])
153
+ else
154
+ raise RuntimeError,
155
+ "Setting comments on unnamed or check constraints is not supported"
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,175 @@
1
+ # Support for setting and retrieving comments on all object types
2
+ # in a PostgreSQL database.
3
+ #
4
+ module Sequel::Extension::PgComment::DatabaseMethods
5
+ # Set the comment for a database object.
6
+ #
7
+ # @param type [#to_s] The type of object that you wish to comment on.
8
+ # This can either be a string or symbol. Any object type that PgSQL
9
+ # knows about should be fair game. The current list of object types
10
+ # that this plugin knows about (and hence will accept) is listed in
11
+ # the {Sequel::Extension::PgComment::OBJECT_TYPES} array.
12
+ #
13
+ # @param id [#to_s] The name of the object that you wish to comment on.
14
+ # For most types of object, this should be the literal name of the
15
+ # object. However, for columns in a table or view, you should separate
16
+ # the table/view name from the column name with a double underscore
17
+ # (ie `__`). This is the standard Sequel convention for such things.
18
+ #
19
+ # @param comment [String] The comment you wish to set for the database
20
+ # object.
21
+ #
22
+ # @see {Sequel::Extension::PgComment.normalise_comment} for details on
23
+ # how the comment string is interpreted.
24
+ #
25
+ def comment_on(type, id, comment)
26
+ gen = begin
27
+ Sequel::Extension::PgComment::SqlGenerator.create(type, id, comment)
28
+ rescue ArgumentError
29
+ raise ArgumentError,
30
+ "Invalid object type: #{type.inspect}"
31
+ end
32
+
33
+ execute(gen.generate)
34
+ end
35
+
36
+ # Retrieve the comment for a database object.
37
+ #
38
+ # @param object [#to_s] The name of the database object to retrieve. For
39
+ # most objects, this should be the literal name of the object.
40
+ # However, for columns on tables and views, the name of the table/view
41
+ # should be a separated from the name of the column by a double
42
+ # underscore (ie `__`).
43
+ #
44
+ # @return [String, NilClass] The comment on the object, or `nil` if no
45
+ # comment has been defined for the object.
46
+ #
47
+ def comment_for(object)
48
+ object = object.to_s
49
+
50
+ if object.index("__")
51
+ tbl, col = object.split("__", 2)
52
+
53
+ execute("SELECT col_description(c.oid, a.attnum) " +
54
+ "FROM pg_class c JOIN pg_attribute a " +
55
+ "ON (c.oid=a.attrelid) " +
56
+ "WHERE c.relname=#{literal(tbl)} " +
57
+ "AND a.attname=#{literal(col)}"
58
+ )
59
+ else
60
+ execute("SELECT obj_description(#{literal(object.to_s)}::regclass, 'pg_class')")
61
+ end
62
+ end
63
+
64
+ # An enhanced form of the standard `create_table` method, which supports
65
+ # setting a comment in the `create_table` call when the `:comment` option
66
+ # is provided.
67
+ #
68
+ # @option [String] :comment The comment to set on the newly-created table.
69
+ #
70
+ # @see [Sequel::Database#create_table](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html#method-i-create_table)
71
+ #
72
+ def create_table(*args)
73
+ super
74
+
75
+ if args.last.is_a?(Hash) && args.last[:comment]
76
+ comment_on(:table, args.first, args.last[:comment])
77
+ end
78
+ end
79
+
80
+ #:nodoc:
81
+ # Enhanced version to support setting comments on objects created in a
82
+ # block-form `create_table` statement.
83
+ #
84
+ def create_table_generator(&block)
85
+ super do
86
+ extend Sequel::Extension::PgComment::CreateTableGeneratorMethods
87
+ @comments = []
88
+ instance_eval(&block) if block
89
+ end
90
+ end
91
+
92
+ #:nodoc:
93
+ # Enhanced version to support setting comments on objects created in a
94
+ # block-form `create_table` statement.
95
+ #
96
+ # If you're wondering why we override the
97
+ # create_table_indexes_from_generator method, rather than
98
+ # create_table_from_generator, it's because the indexes method runs last,
99
+ # and we can only create our comments after the objects we're commenting
100
+ # on have been created. We *could* set some comments in
101
+ # create_table_from_generator, and then set index comments in
102
+ # create_table_indexes_from_generator, but why override two methods when
103
+ # you can just override one to get the same net result?
104
+ #
105
+ def create_table_indexes_from_generator(name, generator, options)
106
+ super
107
+
108
+ generator.comments.each do |sql_gen|
109
+ if sql_gen.respond_to? :table_name
110
+ sql_gen.table_name = name
111
+ end
112
+
113
+ execute(sql_gen.generate)
114
+ end
115
+ end
116
+
117
+ #:nodoc:
118
+ # Enhanced version to support setting comments on objects created in a
119
+ # block-form `alter_table` statement.
120
+ #
121
+ def alter_table_generator(&block)
122
+ super do
123
+ extend Sequel::Extension::PgComment::AlterTableGeneratorMethods
124
+ @comments = []
125
+ instance_eval(&block) if block
126
+ end
127
+ end
128
+
129
+ #:nodoc:
130
+ # Enhanced version to support setting comments on objects created in a
131
+ # block-form `alter_table` statement.
132
+ #
133
+ def apply_alter_table_generator(name, generator)
134
+ super
135
+
136
+ generator.comments.each do |sql_gen|
137
+ if sql_gen.respond_to?(:table_name=)
138
+ sql_gen.table_name = name
139
+ end
140
+
141
+ execute(sql_gen.generate)
142
+ end
143
+ end
144
+
145
+ # An enhanced form of the standard `create_view` method, which supports
146
+ # setting a comment in the `create_view` call when the `:comment` option
147
+ # is provided.
148
+ #
149
+ # @option [String] :comment The comment to set on the newly-created view.
150
+ #
151
+ # @see [Sequel::Database#create_view](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html#method-i-create_view)
152
+ #
153
+ def create_view(*args)
154
+ super
155
+
156
+ if args.last.is_a?(Hash) && args.last[:comment]
157
+ comment_on(:view, args.first, args.last[:comment])
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ # Quote an object name, handling the double underscore convention
164
+ # for separating a column name from its containing object.
165
+ #
166
+ def quote_comment_identifier(id)
167
+ id = id.to_s
168
+ if id.index("__")
169
+ tbl, col = id.split("__", 2)
170
+ quote_identifier(tbl) + "." + quote_identifier(col)
171
+ else
172
+ quote_identifier(id)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,22 @@
1
+ # Support for retrieving column comments from a PostgreSQL database
2
+ # via a dataset. For example:
3
+ #
4
+ # DB[:foo_tbl].comment_for(:some_column)
5
+ #
6
+ # Will retrieve the comment for `foo_tbl.some_column`, if such a
7
+ # column exists.
8
+ #
9
+ module Sequel::Extension::PgComment::DatasetMethods
10
+ # Retrieve the comment for the column named `col` in the "primary" table
11
+ # for this dataset.
12
+ #
13
+ # @param col [#to_s] The name of the column for which to retrieve the
14
+ # comment.
15
+ #
16
+ # @return [String, NilClass] The comment defined for the column, or
17
+ # `nil` if there is no defined comment.
18
+ #
19
+ def comment_for(col)
20
+ db.comment_for("#{first_source_table}__#{col}")
21
+ end
22
+ end
@@ -0,0 +1,257 @@
1
+ module Sequel::Extension::PgComment
2
+ #:nodoc:
3
+ # Generate SQL to set a comment.
4
+ #
5
+ class SqlGenerator
6
+ # The PostgreSQL object types which this class knows how to generate
7
+ # comment SQL for.
8
+ #
9
+ OBJECT_TYPES = %w{AGGREGATE
10
+ CAST
11
+ COLLATION
12
+ CONVERSION
13
+ DATABASE
14
+ DOMAIN
15
+ EXTENSION
16
+ EVENT\ TRIGGER
17
+ FOREIGN\ DATA\ WRAPPER
18
+ FOREIGN\ TABLE
19
+ FUNCTION
20
+ INDEX
21
+ LARGE\ OBJECT
22
+ MATERIALIZED\ VIEW
23
+ OPERATOR
24
+ OPERATOR\ CLASS
25
+ OPERATOR\ FAMILY
26
+ PROCEDURAL\ LANGUAGE
27
+ LANGUAGE
28
+ ROLE
29
+ SCHEMA
30
+ SEQUENCE
31
+ SERVER
32
+ TABLE
33
+ TABLESPACE
34
+ TEXT\ SEARCH\ CONFIGURATION
35
+ TEXT\ SEARCH\ DICTIONARY
36
+ TEXT\ SEARCH\ PARSER
37
+ TEXT\ SEARCH\ TEMPLATE
38
+ TYPE
39
+ VIEW
40
+ }
41
+
42
+ # Find the correct class for a given object type, and instantiate a
43
+ # new one of them.
44
+ #
45
+ # @param object_type [String, Symbol] The type of object we're going
46
+ # to comment on. Strings and symbols are both fine, and any case
47
+ # is fine, too. Any underscores get turned into spaces. Apart from
48
+ # that, it needs to be the exact name that PostgreSQL uses for the given
49
+ # type.
50
+ #
51
+ # @param object_name [String, Symbol] The name of the database object to
52
+ # set the comment on. A string is considered "already quoted", and hence
53
+ # is not escaped any further. A symbol is run through the usual Sequel
54
+ # identifier escaping code before being unleashed on the world.
55
+ #
56
+ # @param comment [String] The comment to set.
57
+ #
58
+ # @return [SqlGenerator] Some sort of `SqlGenerator` object, or a subclass.
59
+ #
60
+ # @raise [ArgumentError] if you passed in an `object_type` that we don't
61
+ # know about.
62
+ #
63
+ def self.create(object_type, object_name, comment)
64
+ generators.each do |gclass|
65
+ if gclass.handles?(object_type)
66
+ return gclass.new(object_type, object_name, comment)
67
+ end
68
+ end
69
+
70
+ raise ArgumentError,
71
+ "Unrecognised object type #{object_type.inspect}"
72
+ end
73
+
74
+ # Return whether or not this class supports the specified object type.
75
+ #
76
+ # @param object_type [String, Symbol] @see {.create}
77
+ #
78
+ # @return [TrueClass, FalseClass] whether or not this class can handle
79
+ # the object type you passed.
80
+ #
81
+ def self.handles?(object_type)
82
+ self.const_get(:OBJECT_TYPES).include?(object_type.to_s.upcase.gsub('_', ' '))
83
+ end
84
+
85
+ private
86
+
87
+ # Return all known `SqlGenerator` classes.
88
+ #
89
+ def self.generators
90
+ @generators ||= ObjectSpace.each_object(Class).select do |klass|
91
+ klass.ancestors.include?(self)
92
+ end
93
+ end
94
+
95
+ # We just need this so we can quote things.
96
+ def self.mock_db
97
+ @mock_db ||= Sequel.connect("mock://postgres")
98
+ end
99
+
100
+ public
101
+
102
+ # The canonicalised string (that is, all-uppercase, with words
103
+ # separated by spaces) for the object type of this SQL generator.
104
+ #
105
+ attr_reader :object_type
106
+
107
+ # The raw (might-be-a-symbol, might-be-a-string) object name that
108
+ # was passed to us originally.
109
+ #
110
+ attr_reader :object_name
111
+
112
+ # The comment.
113
+ attr_reader :comment
114
+
115
+ # Spawn a new SqlGenerator.
116
+ #
117
+ # @see {.create}
118
+ #
119
+ def initialize(object_type, object_name, comment)
120
+ @object_type = object_type.to_s.upcase.gsub('_', ' ')
121
+ @object_name = object_name
122
+ @comment = comment
123
+ end
124
+
125
+ # SQL to set a comment on the object of our affection.
126
+ #
127
+ # @return [String] The SQL needed to set the comment.
128
+ #
129
+ def generate
130
+ quoted_object_name = case object_name
131
+ when Symbol
132
+ literal object_name
133
+ else
134
+ object_name
135
+ end
136
+
137
+ "COMMENT ON #{object_type} #{quoted_object_name} IS #{literal comment.to_s}"
138
+ end
139
+
140
+ private
141
+
142
+ # Quote the provided database object (a symbol) or string value
143
+ # (a string).
144
+ #
145
+ def literal(s)
146
+ self.class.mock_db.literal(s)
147
+ end
148
+ end
149
+
150
+ #:nodoc:
151
+ # A specialised generator for object types that live "inside" a
152
+ # table. Specifically, those types are columns, constraints,
153
+ # rules, and triggers.
154
+ #
155
+ # They get their own subclass because these object types can be
156
+ # manipulated inside a `create_table` or `alter_table` block, and at the
157
+ # time the block is evaluated, the code doesn't know the name of the
158
+ # table in which they are contained. So, we just stuff what we *do* know
159
+ # into these generators, and then when all's done, we can go to each of
160
+ # these generators, say "this is your table name", and then ask for the
161
+ # generated SQL.
162
+ #
163
+ class TableObjectSqlGenerator < SqlGenerator
164
+ # The few object types that this class handles.
165
+ OBJECT_TYPES = %w{COLUMN CONSTRAINT RULE TRIGGER}
166
+
167
+ # The name of the object which contains the object which is the direct
168
+ # target of this SQL generator. Basically, it's the table name.
169
+ attr_accessor :table_name
170
+
171
+ # Overridden constructor to deal with the double-underscore-separated
172
+ # names that we all know and love.
173
+ #
174
+ # @see {SqlGenerator#initialize}
175
+ #
176
+ def initialize(object_type, object_name, comment)
177
+ super
178
+
179
+ if object_name.is_a?(Symbol) and object_name.to_s.index("__")
180
+ @table_name, @object_name = object_name.to_s.split("__", 2).map(&:to_sym)
181
+ end
182
+ end
183
+
184
+ # Generate special SQL.
185
+ #
186
+ # @see {SqlGenerator#generate}
187
+ #
188
+ def generate
189
+ if table_name.nil?
190
+ raise ArgumentError,
191
+ "Cannot generate SQL for #{object_type} #{object_name} " +
192
+ "without a table_name"
193
+ end
194
+
195
+ qualified_object_name = case object_type
196
+ when "COLUMN"
197
+ "#{maybe_escape table_name}.#{maybe_escape object_name}"
198
+ when "CONSTRAINT", "RULE", "TRIGGER"
199
+ "#{maybe_escape object_name} ON #{maybe_escape table_name}"
200
+ end
201
+
202
+ "COMMENT ON #{object_type} #{qualified_object_name} IS #{literal comment}"
203
+ end
204
+
205
+ private
206
+
207
+ # Handle with the vagaries of having both strings and symbols as
208
+ # possible names -- we escape symbols, but leave strings to their own
209
+ # devices.
210
+ #
211
+ def maybe_escape(s)
212
+ Symbol === s ? literal(s) : s
213
+ end
214
+ end
215
+
216
+ #:nodoc:
217
+ # This is an annoying corner-case generator -- it doesn't handle any
218
+ # types by default, but it will handle any *other* type where the name of
219
+ # a table needs to be prefixed by a name. The only known use case for
220
+ # this at present is "implicit" (that is, automatically generated by the
221
+ # database) constraints and indexes that get prefixed by the table name,
222
+ # and which are generated at a time when the calling code doesn't know the
223
+ # name of the table that it is generating SQL for.
224
+ #
225
+ class PrefixSqlGenerator < SqlGenerator
226
+ # This class doesn't handle any object types directly, and must be
227
+ # instantiated directly when needed
228
+ OBJECT_TYPES = %w{}
229
+
230
+ # The name of the table which should be prefixed to the object name
231
+ # that was specified when this instance was created.
232
+ #
233
+ attr_accessor :table_name
234
+
235
+ # Generate super-dooper special SQL.
236
+ #
237
+ # @see {SqlGenerator#generate}
238
+ #
239
+ def generate
240
+ if table_name.nil?
241
+ raise ArgumentError,
242
+ "Cannot generate SQL for #{object_type} #{object_name} " +
243
+ "without a table_name"
244
+ end
245
+
246
+ prefixed_object_name = "#{table_name}#{object_name}"
247
+
248
+ if Symbol === table_name || Symbol === object_name
249
+ prefixed_object_name = prefixed_object_name.to_sym
250
+ end
251
+
252
+ g = SqlGenerator.create(object_type, prefixed_object_name, comment)
253
+ g.table_name = table_name if g.respond_to? :table_name
254
+ g.generate
255
+ end
256
+ end
257
+ end