sequel-pg-comment 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/Gemfile +3 -0
- data/Guardfile +15 -0
- data/LICENCE +674 -0
- data/README.md +209 -0
- data/Rakefile +39 -0
- data/lib/sequel/extensions/pg_comment.rb +73 -0
- data/lib/sequel/extensions/pg_comment/alter_table_generator_methods.rb +126 -0
- data/lib/sequel/extensions/pg_comment/create_table_generator_methods.rb +159 -0
- data/lib/sequel/extensions/pg_comment/database_methods.rb +175 -0
- data/lib/sequel/extensions/pg_comment/dataset_methods.rb +22 -0
- data/lib/sequel/extensions/pg_comment/sql_generator.rb +257 -0
- data/sequel-pg-comment.gemspec +36 -0
- data/spec/alter_table_comment_spec.rb +124 -0
- data/spec/comment_for_spec.rb +34 -0
- data/spec/comment_on_spec.rb +56 -0
- data/spec/create_join_table_comment_spec.rb +20 -0
- data/spec/create_table_comment_spec.rb +193 -0
- data/spec/create_view_comment_spec.rb +18 -0
- data/spec/extension_spec.rb +11 -0
- data/spec/normalise_comment_spec.rb +48 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/sql_generator_spec.rb +96 -0
- metadata +242 -0
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
|