sequel-pg-comment 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ Databases are the ugly step-child of documentation. Explaining what each
2
+ column and table is actually *for* suffers from the "[documentation vicious
3
+ circle](http://www.hezmatt.org/~mpalmer/blog/2015/02/15/the-documentation-vicious-circle.html)"
4
+ -- nobody writes documentation, because nobody reads it, because it never
5
+ exists, therefore there's no point even looking for it. Unlike source code,
6
+ which allows comments to exist alongside the live-edited code, there's
7
+ typically *no way* to keep documentation at the "point of use" of an SQL
8
+ database. Sure, you can write docs in your migrations, or a wiki somewhere,
9
+ but when you're banging away at your SQL command line, who wants to go
10
+ rummaging around in a wiki?
11
+
12
+ As with all things, PostgreSQL to the rescue! The non-standard [`COMMENT`
13
+ command](http://www.postgresql.org/docs/current/interactive/sql-comment.html)
14
+ allows you to attach an arbitrary chunk of text to pretty much any object in
15
+ the database. Want to document a collation? `COMMENT ON COLLATION
16
+ <object_name> IS 'something something dark side'` and you're done!
17
+
18
+ If you're a lover of [Sequel](http://sequel.jeremyevans.net/), though, the
19
+ last thing you want to be doing is hand-writing SQL. Blech. That's old
20
+ school. You want to have your comments Right There in the migrations.
21
+ That's what this gem is all about.
22
+
23
+
24
+ # Usage
25
+
26
+ First, you need to enable the plugin:
27
+
28
+ Sequel::Database.extension :pg_comment
29
+
30
+ Then, you can attach a comment to anything you can create with [Sequel
31
+ schema
32
+ modifications](http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html)
33
+ by adding a `:comment` option, like so:
34
+
35
+ create_table :comments, :comment => "Foo to you too" do
36
+ primary_key :id, :comment => "Auto-incrementing primary key"
37
+ String :data, :null => false, :comment => "Markdown"
38
+ foreign_key :user_id, :users, :comment => "The user who owns the comment"
39
+
40
+ index :user_id, :comment => "Find those user comments faster!"
41
+ end
42
+
43
+ For some object types, though, there's no syntactic sugar in Sequel, so
44
+ you've got to create them by hand. Never fear, though! You can comment on
45
+ any object using Ruby code, like so:
46
+
47
+ comment_on :collation, :my_collation, "Collate ALL THE THINGS!"
48
+
49
+ This is useful also for tables, where you might not want to insert a lengthy
50
+ comment in the top of your `create_table` block.
51
+
52
+ The `table.column` syntax that PgSQL requires for `COMMENT ON COLUMN` can be
53
+ simulated in the usual Sequel fashion, of using two underscores in the
54
+ symbol to separate the table name from the column name, like so:
55
+
56
+ comment_on :column, :foo__bar_id, %{
57
+ This is my column. There are many like it, but this one is mine.
58
+ }
59
+ # => COMMENT ON COLUMN "foo"."bar_id" IS 'This is my column. (etc)'
60
+
61
+ **NOTE**: The object you wish to comment on *must already exist* before you
62
+ call `comment_on`. The following example **WILL NOT WORK**:
63
+
64
+ comment_on :table, :foo, "This is an awesomely foo table"
65
+ create_table :foo, do
66
+ # ...
67
+ end
68
+
69
+ You have to put the `comment_on` *after* the `create_table`, like this:
70
+
71
+ create_table :foo, do
72
+ # ...
73
+ end
74
+ comment_on :table, :foo, "This is an awesomely foo table"
75
+
76
+
77
+ ## Comment string tidy-up
78
+
79
+ On the whole, `sequel-pg-comment` makes no judgment on what you put in your
80
+ comments. Plain text, markdown, XML, or morse code -- it's all the same.
81
+
82
+ There is *one* manipulation that is done to multi-line comments, though, to
83
+ make it a bit easier to write lengthy treatises on the whichness of the why,
84
+ and that is to strip out leading whitespace. The rules are very simple:
85
+
86
+ 1. Empty lines at the beginning and end of the comment are removed; and
87
+
88
+ 1. Whatever whitespace is present before the *first* non-empty line of the
89
+ comment, will be stripped from the beginning of *every* line.
90
+
91
+ That means you can use a heredoc for your multi-line comments, and they'll
92
+ still look neat and tidy without having to play `gsub` tricks:
93
+
94
+ create_table :foo do
95
+ String :data, :comment => <<-EOF
96
+ This is a very lengthy comment. It goes for many lines
97
+ and has a great deal to say on any number of subjects. I
98
+ could have used lorem ipsum here, but I prefer to do things
99
+ the old-fashioned way. If you've read all of this example,
100
+ you probably stay to read the whole of the credits at the
101
+ cinema. Good for you! I do too. Wave next time, you
102
+ anti-social loner.
103
+ EOF
104
+ end
105
+
106
+ One caveat: it's common to use the `%( ... )` quoting style for lengthy
107
+ strings. That's fine, but make sure to put the first line of docs on its
108
+ own line, and not directly after the `%(`. For example, this will not work
109
+ so well:
110
+
111
+ # This WILL NOT trim leading whitespace from each line
112
+ comment_on :table, :foo, %(This is a long comment.
113
+ However, due to the way that pg-comment trims whitespace,
114
+ these lines will have leading indents, because the first line
115
+ didn't.
116
+ )
117
+
118
+ Instead, you'll want to do this:
119
+
120
+ # This WILL trim leading whitespace from each line
121
+ comment_on :table, :foo, %(
122
+ This, too, is a long comment. Because the first non-empty
123
+ line had leading spaces, all of these other lines will have
124
+ their leaving spaces stripped too.
125
+ )
126
+
127
+ As always, inconsistent use of tabs and spaces will end in disaster. So
128
+ don't do that. Remember: tabs are for indenting, spaces are for formatting.
129
+
130
+
131
+ ## Quoting and escaping
132
+
133
+ In normal circumstances, if you follow some fairly simple rules and don't
134
+ need to put comments on a few gnarly types, you should never have to do any
135
+ SQL-specific escaping or quoting of the values you pass to the methods in
136
+ this extension. We work very hard to properly escape as much as we can.
137
+
138
+ The rules for quoting are:
139
+
140
+ 1. If an object name is passed as a symbol, it will be escaped. For certain
141
+ types (`COLUMN`, `CONSTRAINT`, `RULE`, and `TRIGGER`), we split on the first
142
+ double underscore (ie `__`) and the part before the double underscore is the
143
+ table name, and the rest is the object name. Each part is quoted
144
+ separately.
145
+
146
+ 2. If an object name is passed as a string, **NO QUOTING IS PERFORMED**. It
147
+ is assumed, in that instance, that you've already done all the quoting you
148
+ need yourself.
149
+
150
+ 3. Comment strings should always be passed as strings.
151
+
152
+ 4. Object types can be specified as either strings or symbols, with space-
153
+ or understore-separated words, in any mix of case, and they will be
154
+ correctly handled.
155
+
156
+ (Of course, the *real* rule for object naming is: *stick to alphanumerics and
157
+ underscores, fer cryin' out loud!*)
158
+
159
+ The relative complexity of these rules is due to the fact that there are a
160
+ few PostgreSQL object types which stubbornly resist attempts to
161
+ automatically escape their names in a safe manner. The most visible
162
+ offender is `FUNCTION`, which is both relatively commonly used, and has a
163
+ *particularly* complicated object name specification. For those types, you
164
+ have to do some quoting yourself, and pass the object name as a string.
165
+
166
+ I do apologise for the complexity and lack of *absolute* safety in all this
167
+ (although if someone can SQL inject your migrations, you're having a
168
+ *really* bad day), but I had absolutely no luck in producing an interface
169
+ that wasn't a complete nightmare to use, an implementation that wasn't a
170
+ complex mess, and that stuck fairly closely to the common idioms present in
171
+ Sequel proper. If you happen to have ideas on how to make this better, [I'm
172
+ all ears](mailto:theshed+sequel-pg-comment@hezmatt.org).
173
+
174
+
175
+ # Getting your comments back
176
+
177
+ It's great that this gem can help you to document your database, but that's
178
+ not much use if nobody can read them again. Within Sequel itself, you can
179
+ retrieve comments quite easily:
180
+
181
+ DB.comment_for(:foo) # => "Something something dark side"
182
+
183
+ For columns, you can either use the double underscore notation:
184
+
185
+ DB.comment_for(:foo__column) # => "Awwwwww yeah"
186
+
187
+ Or you can do the same thing from the dataset itself:
188
+
189
+ DB[:foo].comment_for(:column) # => "Awwwwww yeah"
190
+
191
+ There's currently no support for retrieving a database comment from a Sequel
192
+ model; pull requests implementing such a feature would be warmly welcomed.
193
+
194
+ The real value in database comments, though, comes when you use an
195
+ entity-relationship diagram tool like
196
+ [SchemaSpy](http://schemaspy.sourceforge.net/), which draws all sorts of
197
+ pretty pictures and lays out all of the schema information.
198
+
199
+ If you quickly need to get some docs when you're in `psql`, you can get it
200
+ out of the "additional detail" informational commands, like `\dt+ <table
201
+ name>`.
202
+
203
+
204
+ # Contributing
205
+
206
+ Bug reports should be sent to the [Github issue
207
+ tracker](https://github.com/mpalmer/sequel-pg-comment/issues), or
208
+ [e-mailed](mailto:theshed+sequel-pg-comment@hezmatt.org). Patches can be sent as a
209
+ Github pull request, or [e-mailed](mailto:theshed+sequel-pg-comment@hezmatt.org).
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ exec(*(["bundle", "exec", $PROGRAM_NAME] + ARGV)) if ENV['BUNDLE_GEMFILE'].nil?
2
+
3
+ Bundler.setup(:default, :development)
4
+
5
+ task :default => :test
6
+
7
+ begin
8
+ Bundler.setup(:default, :development)
9
+ rescue Bundler::BundlerError => e
10
+ $stderr.puts e.message
11
+ $stderr.puts "Run `bundle install` to install missing gems"
12
+ exit e.status_code
13
+ end
14
+
15
+ Bundler::GemHelper.install_tasks
16
+
17
+ task :release do
18
+ sh "git release"
19
+ end
20
+
21
+ require 'yard'
22
+
23
+ YARD::Rake::YardocTask.new :doc do |yardoc|
24
+ yardoc.files = %w{lib/**/*.rb - README.md}
25
+ end
26
+
27
+ desc "Run guard"
28
+ task :guard do
29
+ require 'guard'
30
+ ::Guard.start(:clear => true)
31
+ while ::Guard.running do
32
+ sleep 0.5
33
+ end
34
+ end
35
+
36
+ require 'rspec/core/rake_task'
37
+ RSpec::Core::RakeTask.new :test do |t|
38
+ t.pattern = "spec/**/*_spec.rb"
39
+ end
@@ -0,0 +1,73 @@
1
+ module Sequel::Extension; end #:nodoc:
2
+
3
+ # PostgreSQL-specific extension to set and retrieve comments on
4
+ # all database objects.
5
+ #
6
+ module Sequel::Extension::PgComment
7
+ # Strip indenting whitespace from a comment string.
8
+ #
9
+ # Two rules are applied by this method:
10
+ #
11
+ # 1. Empty lines (that is, lines which have *no* characters in them, not
12
+ # even whitespace) are removed from the beginning and end of the
13
+ # string.
14
+ #
15
+ # 2. Whatever whitespace characters may be present before the first line
16
+ # of text will be removed from the beginning of each subsequent line
17
+ # of the comment, if it exists.
18
+ #
19
+ # This means you can use heredocs and multi-line strings without having
20
+ # to play your own silly-buggers in order to not make the comments look
21
+ # like arse. If you like heredocs, you're encouraged to use this style:
22
+ #
23
+ # comment_for :table, :foo, <<-EOF
24
+ # This is my comment.
25
+ #
26
+ # And here's another line.
27
+ # EOF
28
+ #
29
+ # On the other hand, if percent-quotes are more your cup of tea, you can
30
+ # go at them like this:
31
+ #
32
+ # comment_for :table, :foo, %(
33
+ # This is my comment.
34
+ #
35
+ # And here's another line.
36
+ # )
37
+ #
38
+ # Be sure to not start the first line of your comment on the same line as
39
+ # the quote character, though, because that will mean the first line of
40
+ # the comment *doesn't* have any leading whitespace, and so whitespace
41
+ # won't be trimmed from the rest of the comment. So, don't do this:
42
+ #
43
+ # comment_for :table, :foo, %(This won't work at all well.
44
+ # Since the first line of text doesn't have leading whitespace,
45
+ # these lines won't have it stripped either, and everything will
46
+ # look weird.
47
+ # )
48
+ #
49
+ # @param comment [String] The comment to mangle for whitespace.
50
+ #
51
+ # @return [String] The normalised comment, with whitespace removed.
52
+ #
53
+ def self.normalise_comment(comment)
54
+ comment.tap do |s|
55
+ s.gsub!(/\A\n+/, '')
56
+ s.gsub!(/\n+\Z/, '')
57
+
58
+ (s =~ /\A(\s+)/ and lede = $1) or lede = ''
59
+ s.gsub!(/^#{lede}/, '')
60
+ end
61
+ end
62
+ end
63
+
64
+ require_relative 'pg_comment/sql_generator'
65
+ require_relative 'pg_comment/database_methods'
66
+ require_relative 'pg_comment/dataset_methods'
67
+ require_relative 'pg_comment/create_table_generator_methods'
68
+ require_relative 'pg_comment/alter_table_generator_methods'
69
+
70
+ Sequel::Database.register_extension(:pg_comment) do |db|
71
+ db.extend Sequel::Extension::PgComment::DatabaseMethods
72
+ db.extend_datasets Sequel::Extension::PgComment::DatasetMethods
73
+ end
@@ -0,0 +1,126 @@
1
+ #:nodoc:
2
+ # Enhancements to the standard schema modification methods in a
3
+ # block-form `alter_table` method, to support setting comments via the
4
+ # `:comment` option.
5
+ #
6
+ # Note that not every schema modification method is enhanced in this module;
7
+ # some modifications are implemneted in terms of more fundamental methods,
8
+ # and so do not require their own method here. For example, `add_foreign_key`
9
+ # with a single column is handled by `add_column`, and so doesn't require its
10
+ # own implementation. Rest assured that all schema modification methods
11
+ # *should* accept a `:comment` option, and set a comment in the database. If
12
+ # you find one that doesn't, please file a bug.
13
+ #
14
+ module Sequel::Extension::PgComment::AlterTableGeneratorMethods
15
+ attr_reader :comments
16
+
17
+ include Sequel::Extension::PgComment
18
+
19
+ # Enhanced version of the `add_column` schema modification method,
20
+ # which supports setting a comment on the column.
21
+ #
22
+ # @option [String] :comment The comment to set on the column
23
+ # that is being added.
24
+ #
25
+ def add_column(*args)
26
+ super
27
+
28
+ if args.last.is_a?(Hash) && args.last[:comment]
29
+ comments << SqlGenerator.create(:column, args.first, args.last[:comment])
30
+ end
31
+ end
32
+
33
+ # Enhanced version of the `add_composite_primary_key` schema modification
34
+ # method, which supports setting a comment on the index.
35
+ #
36
+ # @option [String] :comment The comment to set on the index that is being
37
+ # added.
38
+ #
39
+ def add_composite_primary_key(columns, opts)
40
+ super
41
+
42
+ if opts[:comment]
43
+ comments << PrefixSqlGenerator.new(:index, :_pkey, opts[:comment])
44
+ end
45
+ end
46
+
47
+ # Enhanced version of the `add_composite_foreign_key` schema modification
48
+ # method, which supports setting a comment on the constraint.
49
+ #
50
+ # @option [String] :comment The comment to set on the constraint that is being
51
+ # added.
52
+ #
53
+ def add_composite_foreign_key(columns, table, opts)
54
+ super
55
+
56
+ if opts[:comment]
57
+ comments << if opts[:name]
58
+ SqlGenerator.create(:constraint, opts[:name], opts[:comment])
59
+ else
60
+ PrefixSqlGenerator.new(
61
+ :constraint,
62
+ "_#{columns.first}_fkey".to_sym,
63
+ opts[:comment]
64
+ )
65
+ end
66
+ end
67
+ end
68
+
69
+ # Enhanced version of the `add_index` schema modification method, which
70
+ # supports setting a comment on the index.
71
+ #
72
+ # @option [String] :comment The comment to set on the index that is being
73
+ # added.
74
+ #
75
+ def add_index(columns, opts = OPTS)
76
+ if opts[:comment]
77
+ comments << if opts[:name]
78
+ SqlGenerator.create(:index, opts[:name], opts[:comment])
79
+ else
80
+ PrefixSqlGenerator.new(
81
+ :index,
82
+ "_#{[columns].flatten.map(&:to_s).join("_")}_index".to_sym,
83
+ opts[:comment]
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ # Enhanced version of the `add_constraint` schema modification method,
90
+ # which supports setting a comment on the constraint.
91
+ #
92
+ # @option [String] :comment The comment to set on the constraint that is
93
+ # being added.
94
+ #
95
+ def add_constraint(name, *args, &block)
96
+ super
97
+
98
+ opts = args.last.is_a?(Hash) ? args.last : {}
99
+
100
+ if opts[:comment]
101
+ comments << SqlGenerator.create(:constraint, name, opts[:comment])
102
+ end
103
+ end
104
+
105
+ # Enhanced version of the `add_unique_constraint` schema modification
106
+ # method, which supports setting a comment on the index.
107
+ #
108
+ # @option [String] :comment The comment to set on the index that is being
109
+ # added.
110
+ #
111
+ def add_unique_constraint(columns, opts = OPTS)
112
+ super
113
+
114
+ if opts[:comment]
115
+ comments << if opts[:name]
116
+ SqlGenerator.create(:index, opts[:name], opts[:comment])
117
+ else
118
+ PrefixSqlGenerator.new(
119
+ :index,
120
+ "_#{[columns].flatten.map(&:to_s).join("_")}_key".to_sym,
121
+ opts[:comment]
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end