├── .github
└── workflows
│ └── pages.yml
├── .gitignore
├── .pylintrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── config_from_pg
├── __init__.py
├── create_pgconfig_settings_view.sql
├── db.py
└── generate.py
├── docs
├── Makefile
├── __init__.py
├── conf.py
├── index.rst
└── make.bat
├── logo
└── pgconfig-logo.xcf
├── requirements.txt
├── run_server.py
├── tests
├── __init__.py
└── test_pgconfig.py
└── webapp
├── __init__.py
├── config.py
├── config
├── pg10.pkl
├── pg11.pkl
├── pg12.pkl
├── pg13.pkl
├── pg14.pkl
├── pg15.pkl
├── pg16.pkl
└── pg17.pkl
├── pgconfig.py
├── routes.py
├── static
├── logo.png
└── style.css
└── templates
├── _layout_header.html
├── about.html
├── custom_config.html
├── footer.html
├── header.html
├── layout.html
├── param.html
└── param_change.html
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Sphinx documentation to Pages
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | pages:
9 | runs-on: ubuntu-latest
10 | environment:
11 | name: github-pages
12 | url: ${{ steps.deployment.outputs.page_url }}
13 | permissions:
14 | pages: write
15 | id-token: write
16 | steps:
17 | - id: deployment
18 | uses: sphinx-notes/pages@v3
19 | with:
20 | requirements_path: ./requirements.txt
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pytest.xml
2 | **/__pycache__/
3 | docs/_build/
4 | .coverage
5 | *.log
6 | **/.ipynb_checkpoints
7 | *.ipynb
8 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # Specify a configuration file.
4 | #rcfile=
5 |
6 | # Python code to execute, usually for sys.path manipulation such as
7 | # pygtk.require().
8 | #init-hook=
9 |
10 | # Add files or directories to the blacklist. They should be base names, not
11 | # paths.
12 | ignore=CVS
13 |
14 | # Pickle collected data for later comparisons.
15 | persistent=yes
16 |
17 | # List of plugins (as comma separated values of python modules names) to load,
18 | # usually to register additional checkers.
19 | load-plugins=
20 |
21 | # Use multiple processes to speed up Pylint.
22 | jobs=1
23 |
24 | # Allow loading of arbitrary C extensions. Extensions are imported into the
25 | # active Python interpreter and may run arbitrary code.
26 | unsafe-load-any-extension=no
27 |
28 | # A comma-separated list of package or module names from where C extensions may
29 | # be loaded. Extensions are loading into the active Python interpreter and may
30 | # run arbitrary code
31 | extension-pkg-whitelist=
32 |
33 | # Allow optimization of some AST trees. This will activate a peephole AST
34 | # optimizer, which will apply various small optimizations. For instance, it can
35 | # be used to obtain the result of joining multiple strings with the addition
36 | # operator. Joining a lot of strings can lead to a maximum recursion error in
37 | # Pylint and this flag can prevent that. It has one side effect, the resulting
38 | # AST will be different than the one from reality.
39 | optimize-ast=no
40 |
41 |
42 | [MESSAGES CONTROL]
43 |
44 | # Only show warnings with the listed confidence levels. Leave empty to show
45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
46 | confidence=
47 |
48 | # Enable the message, report, category or checker with the given id(s). You can
49 | # either give multiple identifier separated by comma (,) or put this option
50 | # multiple time. See also the "--disable" option for examples.
51 | #enable=
52 |
53 | # Disable the message, report, category or checker with the given id(s). You
54 | # can either give multiple identifiers separated by comma (,) or put this
55 | # option multiple times (only on the command line, not in the configuration
56 | # file where it should appear only once).You can also use "--disable=all" to
57 | # disable everything first and then reenable specific checks. For example, if
58 | # you want to run only the similarities checker, you can use "--disable=all
59 | # --enable=similarities". If you want to run only the classes checker, but have
60 | # no Warning level messages displayed, use"--disable=all --enable=classes
61 | # --disable=W"
62 | #disable=old-octal-literal,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
63 |
64 |
65 | [REPORTS]
66 |
67 | # Set the output format. Available formats are text, parseable, colorized, msvs
68 | # (visual studio) and html. You can also give a reporter class, eg
69 | # mypackage.mymodule.MyReporterClass.
70 | output-format=text
71 |
72 | # Put messages in a separate file for each module / package specified on the
73 | # command line instead of printing them on stdout. Reports (if any) will be
74 | # written in a file name "pylint_global.[txt|html]".
75 | files-output=no
76 |
77 | # Tells whether to display a full report or only the messages
78 | reports=yes
79 |
80 | # Python expression which should return a note less than 10 (10 is the highest
81 | # note). You have access to the variables errors warning, statement which
82 | # respectively contain the number of errors / warnings messages and the total
83 | # number of statements analyzed. This is used by the global evaluation report
84 | # (RP0004).
85 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
86 |
87 | # Template used to display messages. This is a python new-style format string
88 | # used to format the message information. See doc for all details
89 | #msg-template=
90 |
91 |
92 | [VARIABLES]
93 |
94 | # Tells whether we should check for unused import in __init__ files.
95 | init-import=no
96 |
97 | # A regular expression matching the name of dummy variables (i.e. expectedly
98 | # not used).
99 | dummy-variables-rgx=_$|dummy
100 |
101 | # List of additional names supposed to be defined in builtins. Remember that
102 | # you should avoid to define new builtins when possible.
103 | additional-builtins=
104 |
105 | # List of strings which can identify a callback function by name. A callback
106 | # name must start or end with one of those strings.
107 | callbacks=cb_,_cb
108 |
109 |
110 | [LOGGING]
111 |
112 | # Logging modules to check that the string format arguments are in logging
113 | # function parameter format
114 | logging-modules=logging
115 |
116 |
117 | [SIMILARITIES]
118 |
119 | # Minimum lines number of a similarity.
120 | min-similarity-lines=4
121 |
122 | # Ignore comments when computing similarities.
123 | ignore-comments=yes
124 |
125 | # Ignore docstrings when computing similarities.
126 | ignore-docstrings=yes
127 |
128 | # Ignore imports when computing similarities.
129 | ignore-imports=no
130 |
131 |
132 | [SPELLING]
133 |
134 | # Spelling dictionary name. Available dictionaries: none. To make it working
135 | # install python-enchant package.
136 | spelling-dict=
137 |
138 | # List of comma separated words that should not be checked.
139 | spelling-ignore-words=
140 |
141 | # A path to a file that contains private dictionary; one word per line.
142 | spelling-private-dict-file=
143 |
144 | # Tells whether to store unknown words to indicated private dictionary in
145 | # --spelling-private-dict-file option instead of raising a message.
146 | spelling-store-unknown-words=no
147 |
148 |
149 | [TYPECHECK]
150 |
151 | # Tells whether missing members accessed in mixin class should be ignored. A
152 | # mixin class is detected if its name ends with "mixin" (case insensitive).
153 | ignore-mixin-members=yes
154 |
155 | # List of module names for which member attributes should not be checked
156 | # (useful for modules/projects where namespaces are manipulated during runtime
157 | # and thus existing member attributes cannot be deduced by static analysis. It
158 | # supports qualified module names, as well as Unix pattern matching.
159 | ignored-modules=
160 |
161 | # List of classes names for which member attributes should not be checked
162 | # (useful for classes with attributes dynamically set). This supports can work
163 | # with qualified names.
164 | ignored-classes=
165 |
166 | # List of members which are set dynamically and missed by pylint inference
167 | # system, and so shouldn't trigger E1101 when accessed. Python regular
168 | # expressions are accepted.
169 | generated-members=
170 |
171 |
172 | [FORMAT]
173 |
174 | # Maximum number of characters on a single line.
175 | max-line-length=100
176 |
177 | # Regexp for a line that is allowed to be longer than the limit.
178 | ignore-long-lines=^\s*(# )??$
179 |
180 | # Allow the body of an if to be on the same line as the test if there is no
181 | # else.
182 | single-line-if-stmt=no
183 |
184 | # List of optional constructs for which whitespace checking is disabled. `dict-
185 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
186 | # `trailing-comma` allows a space between comma and closing bracket: (a, ).
187 | # `empty-line` allows space-only lines.
188 | no-space-check=trailing-comma,dict-separator
189 |
190 | # Maximum number of lines in a module
191 | max-module-lines=1000
192 |
193 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
194 | # tab).
195 | indent-string=' '
196 |
197 | # Number of spaces of indent required inside a hanging or continued line.
198 | indent-after-paren=4
199 |
200 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
201 | expected-line-ending-format=
202 |
203 |
204 | [MISCELLANEOUS]
205 |
206 | # List of note tags to take in consideration, separated by a comma.
207 | notes=FIXME,XXX,TODO
208 |
209 |
210 | [BASIC]
211 |
212 | # List of builtins function names that should not be used, separated by a comma
213 | bad-functions=map,filter,input
214 |
215 | # Good variable names which should always be accepted, separated by a comma
216 | good-names=i,j,k,ex,Run,_
217 |
218 | # Bad variable names which should always be refused, separated by a comma
219 | bad-names=foo,bar,baz,toto,tutu,tata
220 |
221 | # Colon-delimited sets of names that determine each other's naming style when
222 | # the name regexes allow several styles.
223 | name-group=
224 |
225 | # Include a hint for the correct naming format with invalid-name
226 | include-naming-hint=no
227 |
228 | # Regular expression matching correct function names
229 | function-rgx=[a-z_][a-z0-9_]{2,30}$
230 |
231 | # Naming hint for function names
232 | function-name-hint=[a-z_][a-z0-9_]{2,30}$
233 |
234 | # Regular expression matching correct variable names
235 | variable-rgx=[a-z_][a-z0-9_]{2,30}$
236 |
237 | # Naming hint for variable names
238 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$
239 |
240 | # Regular expression matching correct constant names
241 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
242 |
243 | # Naming hint for constant names
244 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
245 |
246 | # Regular expression matching correct attribute names
247 | attr-rgx=[a-z_][a-z0-9_]{2,30}$
248 |
249 | # Naming hint for attribute names
250 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$
251 |
252 | # Regular expression matching correct argument names
253 | argument-rgx=[a-z_][a-z0-9_]{2,30}$
254 |
255 | # Naming hint for argument names
256 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$
257 |
258 | # Regular expression matching correct class attribute names
259 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
260 |
261 | # Naming hint for class attribute names
262 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
263 |
264 | # Regular expression matching correct inline iteration names
265 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
266 |
267 | # Naming hint for inline iteration names
268 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
269 |
270 | # Regular expression matching correct class names
271 | class-rgx=[A-Z_][a-zA-Z0-9]+$
272 |
273 | # Naming hint for class names
274 | class-name-hint=[A-Z_][a-zA-Z0-9]+$
275 |
276 | # Regular expression matching correct module names
277 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
278 |
279 | # Naming hint for module names
280 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
281 |
282 | # Regular expression matching correct method names
283 | method-rgx=[a-z_][a-z0-9_]{2,30}$
284 |
285 | # Naming hint for method names
286 | method-name-hint=[a-z_][a-z0-9_]{2,30}$
287 |
288 | # Regular expression which should only match function or class names that do
289 | # not require a docstring.
290 | no-docstring-rgx=^_
291 |
292 | # Minimum line length for functions/classes that require docstrings, shorter
293 | # ones are exempt.
294 | docstring-min-length=-1
295 |
296 |
297 | [ELIF]
298 |
299 | # Maximum number of nested blocks for function / method body
300 | max-nested-blocks=5
301 |
302 |
303 | [DESIGN]
304 |
305 | # Maximum number of arguments for function / method
306 | max-args=5
307 |
308 | # Argument names that match this expression will be ignored. Default to name
309 | # with leading underscore
310 | ignored-argument-names=_.*
311 |
312 | # Maximum number of locals for function / method body
313 | max-locals=15
314 |
315 | # Maximum number of return / yield for function / method body
316 | max-returns=6
317 |
318 | # Maximum number of branch for function / method body
319 | max-branches=12
320 |
321 | # Maximum number of statements in function / method body
322 | max-statements=50
323 |
324 | # Maximum number of parents for a class (see R0901).
325 | max-parents=7
326 |
327 | # Maximum number of attributes for a class (see R0902).
328 | max-attributes=7
329 |
330 | # Minimum number of public methods for a class (see R0903).
331 | min-public-methods=2
332 |
333 | # Maximum number of public methods for a class (see R0904).
334 | max-public-methods=20
335 |
336 | # Maximum number of boolean expressions in a if statement
337 | max-bool-expr=5
338 |
339 |
340 | [IMPORTS]
341 |
342 | # Deprecated modules which should not be used, separated by a comma
343 | deprecated-modules=regsub,TERMIOS,Bastion,rexec
344 |
345 | # Create a graph of every (i.e. internal and external) dependencies in the
346 | # given file (report RP0402 must not be disabled)
347 | import-graph=
348 |
349 | # Create a graph of external dependencies in the given file (report RP0402 must
350 | # not be disabled)
351 | ext-import-graph=
352 |
353 | # Create a graph of internal dependencies in the given file (report RP0402 must
354 | # not be disabled)
355 | int-import-graph=
356 |
357 |
358 | [CLASSES]
359 |
360 | # List of method names used to declare (i.e. assign) instance attributes.
361 | defining-attr-methods=__init__,__new__,setUp
362 |
363 | # List of valid names for the first argument in a class method.
364 | valid-classmethod-first-arg=cls
365 |
366 | # List of valid names for the first argument in a metaclass class method.
367 | valid-metaclass-classmethod-first-arg=mcs
368 |
369 | # List of member names, which should be excluded from the protected access
370 | # warning.
371 | exclude-protected=_asdict,_fields,_replace,_source,_make
372 |
373 |
374 | [EXCEPTIONS]
375 |
376 | # Exceptions that will emit a warning when being caught. Defaults to
377 | # "Exception"
378 | overgeneral-exceptions=Exception
379 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We encourage pull requests from everyone.
4 |
5 | You should fork the project into your own repo, create a topic branch there and then make one or more pull requests back to the RustProof Labs repository. Your pull requests will then be reviewed and discussed.
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 - 2024 Ryan Lambert
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # postgresql.conf compare
2 |
3 | Compares parameters defined and their default values between PostgreSQL major versions. [Hosted by RustProof Labs](https://pgconfig.rustprooflabs.com/).
4 |
5 | [API reference](https://rustprooflabs.github.io/pgconfig-ce/index.html)
6 | available.
7 |
8 | ## Deployment Instructions
9 |
10 | > Note: Need to update the sub-version of Python over time. Can use simply
11 | `python3` but that can lead to using older unsupported versions based on distro-defaults.
12 |
13 |
14 | ```bash
15 | cd ~/venv
16 | python3.8 -m venv pgconfig
17 | source ~/venv/pgconfig/bin/activate
18 | ```
19 |
20 | Install requirements
21 |
22 | ```bash
23 | source ~/venv/pgconfig/bin/activate
24 | cd ~/git/pgconfig-ce
25 | pip install -r requirements.txt
26 | ```
27 |
28 | Run web server w/ uWSGI.
29 |
30 | ```bash
31 | source ~/venv/pgconfig/bin/activate
32 | cd ~/git/pgconfig-ce
33 | python run_server.py
34 | ```
35 |
36 | ## Add new config files
37 |
38 | To add a new configuration version you need a Postgres database instance running
39 | that you can connect to. Activate the Python venv and start `ipython`.
40 |
41 | ```bash
42 | source ~/venv/pgconfig/bin/activate
43 | cd ~/git/pgconfig-ce/config_from_pg
44 | ipython
45 | ```
46 |
47 | Import
48 | ```python
49 | import generate
50 | ```
51 |
52 | You'll be prompted for the database connection parameters. Ideally you are using
53 | a `~/.pgpass` file, but the option is there to enter your password.
54 |
55 | ```
56 | Database host [127.0.0.1]:
57 | Database port [5432]:
58 | Database name: postgres
59 | Enter PgSQL username: your_username
60 | Enter password (empty for pgpass):
61 | ```
62 |
63 | Run the generation. Will create a file in the appropriate spot for the webapp.
64 | When adding a new version you need to add it to `webapp/pgconfig.py` as well
65 | as generating this file.
66 |
67 | ```python
68 | generate.run()
69 | ```
70 |
71 | Preparing database objects...
72 | Database objects ready.
73 | Pickled config data saved to: ../webapp/config/pg15.pkl
74 |
75 |
76 |
77 | ## Unit tests
78 |
79 | Run unit tests.
80 |
81 | ```
82 | python -m unittest tests/*.py
83 | ```
84 |
85 | Or run unit tests with coverage.
86 |
87 | ```
88 | coverage run -m unittest tests/*.py
89 | ```
90 |
91 | Generate report.
92 |
93 | ```
94 | coverage report -m webapp/*.py
95 |
96 | Name Stmts Miss Cover Missing
97 | --------------------------------------------------
98 | webapp/__init__.py 15 0 100%
99 | webapp/config.py 25 0 100%
100 | webapp/forms.py 6 0 100%
101 | webapp/pgconfig.py 150 37 75% 26-27, 53, 71-73, 87-94, 115-122, 140-145, 162-170, 222-223, 300, 393, 417
102 | webapp/routes.py 83 58 30% 20, 24, 30, 35, 40-43, 51, 56-75, 87, 92-119, 127-140, 143-155
103 | --------------------------------------------------
104 | TOTAL 279 95 66%
105 | ```
106 |
107 | ## Pylint
108 |
109 | Run pylint.
110 |
111 | ```
112 | pylint --rcfile=./.pylintrc -f parseable ./webapp/*.py ./config_from_pg/*.py
113 | ```
114 |
115 | ## History
116 |
117 | The open source (Community Edition) version of this project started as a manual fork
118 | of RustProof Labs' internal project, commit `fcc0619`. The original internal project will
119 | be retired as this project evolves.
120 |
121 |
--------------------------------------------------------------------------------
/config_from_pg/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/config_from_pg/__init__.py
--------------------------------------------------------------------------------
/config_from_pg/create_pgconfig_settings_view.sql:
--------------------------------------------------------------------------------
1 |
2 | DROP VIEW IF EXISTS pgconfig.settings;
3 | CREATE VIEW pgconfig.settings AS
4 | SELECT name, setting, unit, context, source, category,
5 | reset_val, boot_val,
6 | CASE WHEN vartype IN ('string', 'enum')
7 | THEN
8 | name || ' = ' || CHR(39) || current_setting(name) || CHR(39)
9 | ELSE
10 | name || ' = ' || current_setting(name)
11 | END AS postgresconf_line,
12 | name || ' = ' || CHR(39) ||
13 | -- Recalculates 8kB units to more comprehensible kB units.
14 | -- above line gets to use the current_setting() func, didn't find any
15 | -- such option for this one
16 | CASE WHEN boot_val = '-1' THEN boot_val
17 | WHEN unit = '8kB' THEN ((boot_val::numeric * 8)::BIGINT)::TEXT
18 | ELSE boot_val
19 | END
20 | || COALESCE(CASE WHEN boot_val = '-1' THEN NULL
21 | WHEN unit = '8kB' THEN 'kB'
22 | ELSE unit
23 | END, '') || CHR(39)
24 | AS default_config_line,
25 | short_desc,
26 | CASE WHEN name LIKE 'lc%'
27 | THEN True
28 | WHEN name LIKE 'unix%'
29 | THEN True
30 | WHEN name IN ('application_name', 'TimeZone', 'timezone_abbreviations',
31 | 'default_text_search_config')
32 | THEN True
33 | WHEN category IN ('File Locations')
34 | THEN True
35 | ELSE False
36 | END AS frequent_override,
37 | CASE WHEN boot_val = setting THEN True
38 | ELSE False
39 | END AS system_default,
40 | CASE WHEN reset_val = setting THEN False
41 | ELSE True
42 | END AS session_override,
43 | pending_restart,
44 | vartype, min_val, max_val, enumvals
45 | FROM pg_catalog.pg_settings
46 | ;
47 |
48 | COMMENT ON COLUMN pgconfig.settings.postgresconf_line IS 'Current configuration in format suitable for postgresql.conf. All setting values quoted in single quotes since that always works, and omitting the quotes does not. Uses pg_catalog.current_setting() which converts settings into sensible units for display.';
49 | COMMENT ON COLUMN pgconfig.settings.default_config_line IS 'Postgres default configuration for setting. Some are hard coded, some are determined at build time.';
50 | COMMENT ON COLUMN pgconfig.settings.setting IS 'Raw setting value in the units defined in the "units" column.';
51 |
--------------------------------------------------------------------------------
/config_from_pg/db.py:
--------------------------------------------------------------------------------
1 | """Database helper module.
2 | Modified from https://gist.github.com/rustprooflabs/3b8564a8e7b7fe611436b30a95b7cd17,
3 | adapted to psycopg 3 from psycopg2.
4 | """
5 | import getpass
6 | import psycopg
7 |
8 |
9 | def prepare():
10 | """Ensures latest `pgconfig.settings` view exists in DB to generate config.
11 | """
12 | print('Preparing database objects...')
13 | ensure_schema_exists()
14 | ensure_view_exists()
15 | print('Database objects ready.')
16 |
17 |
18 | def ensure_schema_exists():
19 | """Ensures the `pgconfig` schema exists."""
20 | sql_raw = 'CREATE SCHEMA IF NOT EXISTS pgconfig;'
21 | _execute_query(sql_raw, params=None, qry_type='ddl')
22 |
23 |
24 | def ensure_view_exists():
25 | """Ensures the view `pgconfig.settings` exists."""
26 | sql_file = 'create_pgconfig_settings_view.sql'
27 | with open(sql_file) as f:
28 | sql_raw = f.read()
29 |
30 | _execute_query(sql_raw, params=None, qry_type='ddl')
31 |
32 |
33 | def select_one(sql_raw: str, params: dict) -> dict:
34 | """ Runs SELECT query that will return zero or 1 rows.
35 |
36 | Parameters
37 | -----------------
38 | sql_raw : str
39 | params : dict
40 | Params is required, can be `None` if query returns a single row
41 | such as `SELECT version();`
42 |
43 | Returns
44 | -----------------
45 | data : dict
46 | """
47 | return _execute_query(sql_raw, params, 'sel_single')
48 |
49 |
50 | def select_multi(sql_raw, params=None) -> list:
51 | """ Runs SELECT query that will return multiple. `params` is optional.
52 |
53 | Parameters
54 | -----------------
55 | sql_raw : str
56 | params : dict
57 | Params is optional, defaults to `None`.
58 |
59 | Returns
60 | ------------------
61 | data : list
62 | List of dictionaries.
63 | """
64 | return _execute_query(sql_raw, params, 'sel_multi')
65 |
66 |
67 | def get_db_string() -> str:
68 | """Prompts user for details to create connection string
69 |
70 | Returns
71 | ------------------------
72 | database_string : str
73 | """
74 | db_host = input('Database host [127.0.0.1]: ') or '127.0.0.1'
75 | db_port = input('Database port [5432]: ') or '5432'
76 | db_name = input('Database name: ')
77 | db_user = input('Enter PgSQL username: ')
78 | db_pw = getpass.getpass('Enter password (empty for pgpass): ') or None
79 |
80 | if db_pw is None:
81 | database_string = 'postgresql://{user}@{host}:{port}/{dbname}'
82 | else:
83 | database_string = 'postgresql://{user}:{pw}@{host}:{port}/{dbname}'
84 |
85 | return database_string.format(user=db_user, pw=db_pw, host=db_host,
86 | port=db_port, dbname=db_name)
87 |
88 | DB_STRING = get_db_string()
89 |
90 | def get_db_conn():
91 | """Uses DB_STRING to establish psycopg connection."""
92 | db_string = DB_STRING
93 |
94 | try:
95 | conn = psycopg.connect(db_string)
96 | except psycopg.OperationalError as err:
97 | err_msg = f'DB Connection Error - Error: {err}'
98 | print(err_msg)
99 | return False
100 |
101 | return conn
102 |
103 |
104 | def _execute_query(sql_raw, params, qry_type):
105 | """ Handles executing all types of queries based on the `qry_type` passed in.
106 | Returns False if there are errors during connection or execution.
107 | if results == False:
108 | print('Database error')
109 | else:
110 | print(results)
111 | You cannot use `if not results:` b/c 0 results is a false negative.
112 | """
113 | try:
114 | conn = get_db_conn()
115 | except psycopg.ProgrammingError as err:
116 | print(f'Connection not configured properly. Err: {err}')
117 | return False
118 |
119 | if not conn:
120 | return False
121 |
122 | cur = conn.cursor(row_factory=psycopg.rows.dict_row)
123 |
124 | try:
125 | cur.execute(sql_raw, params)
126 | if qry_type == 'sel_single':
127 | results = cur.fetchone()
128 | elif qry_type == 'sel_multi':
129 | results = cur.fetchall()
130 | elif qry_type == 'ddl':
131 | conn.commit()
132 | results = True
133 | else:
134 | raise Exception('Invalid query type defined.')
135 |
136 | except psycopg.BINARYProgrammingError as err:
137 | print('Database error via psycopg. %s', err)
138 | results = False
139 | except psycopg.IntegrityError as err:
140 | print('PostgreSQL integrity error via psycopg. %s', err)
141 | results = False
142 | finally:
143 | conn.close()
144 |
145 | return results
146 |
--------------------------------------------------------------------------------
/config_from_pg/generate.py:
--------------------------------------------------------------------------------
1 | """Generates config based on pgconfig.settings and pickles for reuse in webapp.
2 |
3 | This code is expected to be used on Postgres 10 and newer.
4 | """
5 | import pickle
6 | import db
7 |
8 |
9 | def run():
10 | """Saves pickled config data from defined database connection.
11 | """
12 | db.prepare()
13 | pg_version_num = get_pg_version_num()
14 | pg_config_data = get_config_data()
15 | save_config_data(data=pg_config_data, pg_version_num=pg_version_num)
16 |
17 |
18 | def get_pg_version_num() -> int:
19 | """Returns the Postgres version number as an integer.
20 |
21 | Expected to be used on Postgres 10 and newer only.
22 |
23 | Returns
24 | ------------------------
25 | pg_version_num : int
26 | """
27 | sql_raw = """SELECT current_setting('server_version_num')::BIGINT / 10000
28 | AS pg_version_num;
29 | """
30 | results = db.select_one(sql_raw, params=None)
31 | pg_version_num = results['pg_version_num']
32 | return pg_version_num
33 |
34 |
35 | def get_config_data() -> list:
36 | """Query Postgres for data about default settings.
37 |
38 | Returns
39 | --------------------
40 | results : list
41 | """
42 | sql_raw = """
43 | SELECT default_config_line, name, unit, context, category,
44 | boot_val, short_desc, frequent_override,
45 | vartype, min_val, max_val, enumvals,
46 | boot_val || COALESCE(' ' || unit, '') AS boot_val_display
47 | FROM pgconfig.settings
48 | /* Excluding read-only present options. Not included in delivered
49 | postgresql.conf files per docs:
50 | https://www.postgresql.org/docs/current/runtime-config-preset.html
51 | */
52 | WHERE category != 'Preset Options'
53 | /* Configuration options that typically are customized such as
54 | application_name do not make sense to compare against "defaults"
55 | */
56 | AND NOT frequent_override
57 | ORDER BY name
58 | ;
59 | """
60 | results = db.select_multi(sql_raw)
61 | return results
62 |
63 | def save_config_data(data: list, pg_version_num: int):
64 | """Pickles config data for reuse later.
65 |
66 | Parameters
67 | ----------------------
68 | data : list
69 | List of dictionaries to pickle.
70 |
71 | pg_version_num : int
72 | Integer of Postgres version.
73 | """
74 | filename = f'../webapp/config/pg{pg_version_num}.pkl'
75 | with open(filename, 'wb') as data_file:
76 | pickle.dump(data, data_file)
77 | print(f'Pickled config data saved to: {filename}')
78 |
79 |
80 | if __name__ == "__main__":
81 | run()
82 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/docs/__init__.py
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # http://www.sphinx-doc.org/en/master/config
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 | sys.path.insert(0, os.path.abspath('../'))
16 |
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = 'pgConfig'
21 | copyright = '2018 - 2024, Ryan Lambert, RustProof Labs'
22 | author = 'Ryan Lambert, RustProof Labs'
23 |
24 | version = '0.1.0'
25 |
26 |
27 | # -- General configuration ---------------------------------------------------
28 |
29 | # Add any Sphinx extension module names here, as strings. They can be
30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31 | # ones.
32 | extensions = ['sphinx.ext.autodoc',
33 | 'sphinx.ext.coverage',
34 | 'sphinx.ext.napoleon',
35 | 'sphinx.ext.viewcode'
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # List of patterns, relative to source directory, that match files and
42 | # directories to ignore when looking for source files.
43 | # This pattern also affects html_static_path and html_extra_path.
44 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
45 |
46 |
47 | # -- Options for HTML output -------------------------------------------------
48 |
49 | # The theme to use for HTML and HTML Help pages. See the documentation for
50 | # a list of builtin themes.
51 | #
52 | html_theme = 'sphinx_rtd_theme'
53 |
54 | # Add any paths that contain custom static files (such as style sheets) here,
55 | # relative to this directory. They are copied after the builtin static files,
56 | # so a file named "default.css" will overwrite the builtin "default.css".
57 | html_static_path = ['_static']
58 |
59 | html_show_sphinx = False
60 |
61 | html_theme_options = {
62 | 'canonical_url': '',
63 | 'logo_only': False,
64 | 'display_version': True
65 | }
66 |
67 | extensions.append('autoapi.extension')
68 | autoapi_type = 'python'
69 | autoapi_dirs = ['../config_from_pg', '../webapp']
70 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. pgConfig documentation master file, created by
2 | sphinx-quickstart on Mon Sep 23 14:47:37 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | pgConfig documentation!
7 | ====================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 |
14 |
15 | Indices and tables
16 | ==================
17 |
18 | * :ref:`genindex`
19 | * :ref:`modindex`
20 | * :ref:`search`
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/logo/pgconfig-logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/logo/pgconfig-logo.xcf
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Click==8.1.7
2 | coverage==7.3.1
3 | Flask==3.0.0
4 | Flask-WTF==1.2.1
5 | itsdangerous==2.1.2
6 | Jinja2==3.1.3
7 | MarkupSafe==2.1.3
8 | numpy==1.26.1
9 | pandas==2.1.2
10 | psycopg==3.1.16
11 | psycopg-binary==3.1.16
12 | pylint==2.17.5
13 | pytest==7.4.3
14 | python-dateutil==2.8.2
15 | pytz==2023.3.post1
16 | six==1.16.0
17 | Sphinx==7.2.6
18 | sphinx-autoapi==2.1.1
19 | sphinx-rtd-theme==1.3.0
20 | uWSGI==2.0.22
21 | Werkzeug==3.0.1
22 | WTForms==3.0.1
23 |
--------------------------------------------------------------------------------
/run_server.py:
--------------------------------------------------------------------------------
1 | from webapp import app
2 |
3 | if __name__ == '__main__':
4 | app.run(host='0.0.0.0')
5 |
6 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_pgconfig.py:
--------------------------------------------------------------------------------
1 | """ Unit tests to cover the pgconfig module."""
2 | import unittest
3 | import pandas as pd
4 | from webapp import pgconfig
5 |
6 |
7 | class PGConfigTests(unittest.TestCase):
8 |
9 | def test_pgconfig_check_redirect_returns_same_version_with_no_redirect(self):
10 | version = '10'
11 | expected = version
12 | new_version = pgconfig.check_redirect(version)
13 | self.assertEqual(expected, new_version)
14 |
15 | def test_pgconfig_check_redirect_returns_updated_redirect_version(self):
16 | version = '12beta4'
17 | expected = '12'
18 | new_version = pgconfig.check_redirect(version)
19 | self.assertEqual(expected, new_version)
20 |
21 |
22 | def test_pgconfig_config_changes_returns_expected_type(self):
23 | vers1 = 12
24 | vers2 = 16
25 | result = pgconfig.config_changes(vers1=vers1, vers2=vers2)
26 | actual = type(result)
27 | expected = pd.DataFrame
28 | self.assertEqual(expected, actual)
29 |
30 | def test_pgconfig_config_changes_raises_ValueError_when_misordered_versions(self):
31 | vers1 = 16
32 | vers2 = 12
33 | self.assertRaises(ValueError, pgconfig.config_changes, vers1, vers2)
34 |
35 | def test_load_config_data_raises_ValueError_when_invalid_version(self):
36 | vers1 = -999
37 | self.assertRaises(ValueError, pgconfig.load_config_data, vers1)
38 |
--------------------------------------------------------------------------------
/webapp/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flask import Flask
3 | from webapp import config
4 |
5 | # App settings
6 | app = Flask(__name__)
7 | app.config['DEBUG'] = config.APP_DEBUG
8 | app.config['SECRET_KEY'] = config.APP_SECRET_KEY
9 | app.config['WTF_CSRF_ENABLED'] = True
10 |
11 | # Setup Logging
12 | LOG_PATH = config.LOG_PATH
13 | LOGGER = logging.getLogger(__name__)
14 | HANDLER = logging.FileHandler(filename=LOG_PATH, mode='a+')
15 | FORMATTER = logging.Formatter(config.LOG_FORMAT)
16 | HANDLER.setFormatter(FORMATTER)
17 | LOGGER.addHandler(HANDLER)
18 | LOGGER.setLevel(logging.DEBUG)
19 |
20 | from webapp import routes
21 |
--------------------------------------------------------------------------------
/webapp/config.py:
--------------------------------------------------------------------------------
1 | """Config module for PgConfig webapp."""
2 | import os
3 | import logging
4 |
5 | CURR_PATH = os.path.abspath(os.path.dirname(__file__))
6 | PROJECT_BASE_PATH = os.path.abspath(os.path.join(CURR_PATH, os.pardir))
7 |
8 | APP_NAME = 'PG Configuration Tracking'
9 |
10 | LOG_FORMAT = '%(levelname)s - %(asctime)s - %(name)s - %(message)s'
11 |
12 | LOGGER = logging.getLogger(__name__)
13 |
14 | try:
15 | LOG_PATH = os.environ['LOG_PATH']
16 | except KeyError:
17 | LOG_PATH = PROJECT_BASE_PATH + '/pgconfig_app.log'
18 |
19 | try:
20 | APP_DEBUG = os.environ['APP_DEBUG']
21 | except KeyError:
22 | APP_DEBUG = True
23 |
24 |
25 | # Required for CSRF protection in Flask, please change to something secret!
26 | try:
27 | APP_SECRET_KEY = os.environ['APP_SECRET_KEY']
28 | except KeyError:
29 | ERR_MSG = '\nSECURITY WARNING: To ensure security please set the APP_SECRET_KEY'
30 | ERR_MSG += ' environment variable.\n'
31 | LOGGER.warning(ERR_MSG)
32 | print(ERR_MSG)
33 | APP_SECRET_KEY = 'YourApplicationShouldBeServedFreshWithASecureAndSecretKey!'
34 |
35 |
36 | SESSION_COOKIE_SECURE = True
37 | REMEMBER_COOKIE_SECURE = True
38 |
--------------------------------------------------------------------------------
/webapp/config/pg10.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg10.pkl
--------------------------------------------------------------------------------
/webapp/config/pg11.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg11.pkl
--------------------------------------------------------------------------------
/webapp/config/pg12.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg12.pkl
--------------------------------------------------------------------------------
/webapp/config/pg13.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg13.pkl
--------------------------------------------------------------------------------
/webapp/config/pg14.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg14.pkl
--------------------------------------------------------------------------------
/webapp/config/pg15.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg15.pkl
--------------------------------------------------------------------------------
/webapp/config/pg16.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg16.pkl
--------------------------------------------------------------------------------
/webapp/config/pg17.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rustprooflabs/pgconfig-ce/10946d43989c3c26fb6ec24f6f85a1a212d48d41/webapp/config/pg17.pkl
--------------------------------------------------------------------------------
/webapp/pgconfig.py:
--------------------------------------------------------------------------------
1 | """Parses PostgreSQL config data from pickled files"""
2 | import os
3 | import logging
4 | import pandas as pd
5 | import pickle
6 |
7 |
8 | LOGGER = logging.getLogger(__name__)
9 | VERSIONS = ['10', '11', '12', '13', '14', '15', '16', '17']
10 | """list : Versions with data included. Add new versions to this list.
11 | When including pre-production versions include a non-production designation,
12 | e.g. 16beta1
13 | """
14 |
15 | VERSION_REDIRECTS = [{'version': '12beta4', 'redirect': '12'},
16 | {'version': '16beta1', 'redirect': '16'}
17 | ]
18 | """list : List of dictionaries. Dict keys are 'version' and 'redirect'."""
19 |
20 | NEW_STRING = 'Configuration parameter added'
21 | REMOVED_STRING = 'Configuration parameter removed'
22 |
23 |
24 | def check_redirect(version):
25 | """Checks version for defined redirects.
26 |
27 | e.g. 12beta4 redirects to 12 once the production-ready version
28 | is released.
29 |
30 | Parameters
31 | ---------------
32 | version : str
33 | Version to check for redirects for.
34 |
35 | Returns
36 | ---------------
37 | version : str
38 | Redirected if necessary, original version if not.
39 | """
40 | for redirect in VERSION_REDIRECTS:
41 | if version == redirect['version']:
42 | LOGGER.info('Redirecting version %s to %s', version,
43 | redirect['redirect'])
44 | return redirect['redirect']
45 | return version
46 |
47 |
48 | def config_changes(vers1: int, vers2: int) -> pd.DataFrame:
49 | """Find changes between `vers1` and `vers2`.
50 |
51 | Parameters
52 | ----------------
53 | vers1 : int
54 | Version number, e.g. 11 or 16
55 |
56 | vers2 : int
57 | Version number, e.g. 11 or 16
58 |
59 | Returns
60 | -----------------
61 | changed : pd.DataFrame
62 | """
63 | if vers2 <= vers1:
64 | raise ValueError('Version 1 must be lower (before) version 2.')
65 |
66 | # Have to drop enumvals in comparison to avoid errors comparing using pandas
67 | data1 = load_config_data(pg_version=vers1)
68 | data2 = load_config_data(pg_version=vers2)
69 |
70 | data2 = data2.add_suffix('2')
71 |
72 | combined = pd.concat([data1, data2], axis=1)
73 |
74 | combined['summary'] = combined.apply(classify_changes, axis=1)
75 | combined['change_display'] = combined.apply(calculate_change_display, axis=1)
76 |
77 | # Create combined columns. Passing in 2nd version first displays latest
78 | # version if there are any differences.
79 | squash_column_names = ['short_desc2', 'short_desc']
80 | new_column = 'short_desc'
81 | combined = squash_columns(data=combined,
82 | original_columns=squash_column_names,
83 | new_column=new_column)
84 |
85 | squash_column_names = ['frequent_override2', 'frequent_override']
86 | new_column = 'frequent_override'
87 | combined = squash_columns(data=combined,
88 | original_columns=squash_column_names,
89 | new_column=new_column)
90 |
91 | squash_column_names = ['category2', 'category']
92 | new_column = 'category'
93 | combined = squash_columns(data=combined,
94 | original_columns=squash_column_names,
95 | new_column=new_column)
96 |
97 | squash_column_names = ['history_url2', 'history_url']
98 | new_column = 'history_url'
99 | combined = squash_columns(data=combined,
100 | original_columns=squash_column_names,
101 | new_column=new_column)
102 |
103 | # Limit the columns
104 | columns = ['summary', 'frequent_override',
105 | 'category', 'short_desc',
106 | 'vartype', 'vartype2',
107 | 'boot_val_display', 'boot_val_display2',
108 | 'enumvals', 'enumvals2', 'change_display',
109 | 'history_url'
110 | ]
111 | changed = combined[combined['summary'] != ''][columns]
112 |
113 | return changed
114 |
115 |
116 | def squash_columns(data: pd.DataFrame, original_columns: list, new_column: str):
117 | """Coalesces the values from DataFrame columns in `original_columns` list
118 | into the `new_column` name. Drops `original_columns`, so can reuse one of
119 | the column names if desired. e.g `short_desc` and `short_desc2` combined
120 | into the `short_desc` column.
121 |
122 | Note: This is useful for added and removed items, NOT changed items.
123 |
124 | Parameters
125 | ---------------------
126 | data : pd.DataFrame
127 | original_columns : list
128 | new_column : str
129 |
130 | Returns
131 | ---------------------
132 | data_new : pd.DataFrame
133 | """
134 | data['tmp'] = data[original_columns].bfill(axis=1).iloc[:, 0]
135 | data_mid = data.drop(columns=original_columns)
136 | data_new = data_mid.rename(columns={'tmp': new_column})
137 | return data_new
138 |
139 |
140 | def load_config_data(pg_version: int) -> pd.DataFrame:
141 | """Loads the pickled config data for `pg_version` into DataFrame with the
142 | config name as the index.
143 |
144 | Returns empty DataFrame on file read error.
145 |
146 | Parameters
147 | ----------------------
148 | pg_version : int
149 |
150 | Returns
151 | ----------------------
152 | df : pd.DataFrame
153 | """
154 | base_path = os.path.dirname(os.path.realpath(__file__))
155 | # Checking user input against configured versions to avoid security concerns
156 | if str(pg_version) not in VERSIONS:
157 | raise ValueError(f'Invalid Postgres version. Options are {VERSIONS}')
158 |
159 | filename = os.path.join(base_path, 'config', f'pg{pg_version}.pkl')
160 |
161 | try:
162 | with open(filename, 'rb') as data_file:
163 | config_data = pickle.load(data_file)
164 |
165 | df = pd.DataFrame(config_data)
166 | except FileNotFoundError:
167 | msg = f'File not found for Postgres version {pg_version}'
168 | print(msg)
169 | LOGGER.error(msg)
170 | df = pd.DataFrame()
171 | return df
172 |
173 | # Add hyperlink to the parameter history page
174 | html_part1 = ''
176 | df['history_url'] = html_part1 + df['name'] + html_part2
177 |
178 | df.set_index('name', inplace=True)
179 | return df
180 |
181 |
182 | def is_NaN(input: str) -> bool:
183 | """Checks string values for NaN, aka it isn't equal to itself.
184 |
185 | Parameters
186 | ------------------------
187 | input : str
188 |
189 | Returns
190 | ------------------------
191 | is_nan : bool
192 | """
193 | return input != input
194 |
195 |
196 | def classify_changes(row: pd.Series) -> str:
197 | """Used by dataFrame.apply on the combined DataFrame to check version1 and
198 | version2 values, types, etc. for differences.
199 |
200 | Parameters
201 | --------------------------
202 | row : pd.Series
203 | Row from combined DataFrame to check details.
204 |
205 | Returns
206 | -------------------------
207 | changes : str
208 | Changes are built as a list internally and returned as a string
209 | with a comma separated list of changes.
210 | """
211 | changes = []
212 | delim = ', '
213 |
214 | # When old is empty, and new is not, it's a new parameters
215 | if is_NaN(row['default_config_line']) and not is_NaN(row['default_config_line2']):
216 | changes.append('Configuration parameter added')
217 | return delim.join(changes)
218 |
219 | # When new is empty and old is not, it was removed
220 | if is_NaN(row['default_config_line2']) and not is_NaN(row['default_config_line']):
221 | changes.append('Configuration parameter removed')
222 | return delim.join(changes)
223 |
224 | if row['boot_val'] != row['boot_val2']:
225 | changes.append('Changed default value')
226 | if row['vartype'] != row['vartype2']:
227 | changes.append('Changed variable type')
228 | return delim.join(changes)
229 |
230 |
231 | def calculate_change_display(row: pd.Series) -> str:
232 | """Used by dataFrame.apply on the combined DataFrame to create the columns
233 | to display for changes.
234 |
235 | Parameters
236 | --------------------------
237 | row : pd.Series
238 | Row from combined DataFrame to check details.
239 |
240 | Returns
241 | -------------------------
242 | changes : str
243 | Changes are built as a list internally and returned as a string
244 | with a comma separated list of changes.
245 | """
246 | changes = []
247 | delim = ', '
248 |
249 | # If either is Nan, don't calculate
250 | if is_NaN(row['default_config_line']) or is_NaN(row['default_config_line2']):
251 | return None
252 |
253 | if row.boot_val != row.boot_val2:
254 | changes.append(f'Default value: {row.boot_val} -> {row.boot_val2}')
255 | if row['vartype'] != row['vartype2']:
256 | changes.append(f'Variable type: {row.vartype}
-> {row.vartype2}
')
257 | return delim.join(changes)
258 |
259 |
260 | def config_changes_html(changes: pd.DataFrame) -> dict:
261 | """Splits `changes` data into new, removed, and changed.
262 |
263 | Parameters
264 | -------------------
265 | changes : pd.DataFrame
266 |
267 | Returns
268 | -------------------
269 | changes_html : dict
270 | Dictionary with keys:
271 | * new
272 | * removed
273 | * changed
274 |
275 | Each item holds the string HTML for the table of the data from input
276 | DataFrame
277 | """
278 | # New Section
279 | columns_new = ['category', 'short_desc', 'boot_val_display2',
280 | 'vartype2', 'enumvals2', 'history_url']
281 | rename_columns_new = {'vartype2': 'Var Type',
282 | 'boot_val_display2': 'Default Value',
283 | 'enumvals2': 'Enum Values'
284 | }
285 | new = changes[changes.summary == NEW_STRING][columns_new].rename(columns=rename_columns_new)
286 |
287 | new_html = _df_to_html(new)
288 |
289 | # Removed Section
290 | columns_removed = ['category', 'short_desc', 'boot_val_display',
291 | 'vartype', 'enumvals', 'history_url']
292 | rename_columns_removed = {'vartype': 'Var Type',
293 | 'boot_val_display': 'Default Value',
294 | 'enumvals': 'Enum Values'
295 | }
296 | removed = changes[changes.summary == REMOVED_STRING][columns_removed].rename(columns=rename_columns_removed)
297 | removed_html = _df_to_html(removed)
298 |
299 | # Changed section
300 | columns_changed = ['category', 'short_desc', 'change_display',
301 | 'history_url']
302 | rename_columns_changed = {'vartype': 'Var Type',
303 | 'change_display': 'Changed:',
304 | 'enumvals': 'Enum Values'
305 | }
306 | changed = changes[~changes.summary.isin([NEW_STRING, REMOVED_STRING])][columns_changed].rename(columns=rename_columns_changed)
307 | changed_html = _df_to_html(changed)
308 |
309 | return {'new': new_html, 'removed': removed_html, 'changed': changed_html}
310 |
311 |
312 | def config_changes_stats(changes: dict) -> dict:
313 | """Provides counts of changes by type (new, removed, updated) to display.
314 |
315 | Parameters
316 | ---------------------
317 | changes : dict
318 |
319 | Returns
320 | ---------------------
321 | stats : dict
322 | """
323 | new = changes[changes.summary == NEW_STRING].count().max()
324 | removed = changes[changes.summary == REMOVED_STRING].count().max()
325 | total = changes.count().max()
326 | updated = total - new - removed
327 | stats = {'new': new,
328 | 'updated': updated,
329 | 'removed': removed}
330 | return stats
331 |
332 |
333 | def _df_to_html(df):
334 | """Converts DataFrame to HTML table with classes for formatting.
335 |
336 | Parameters
337 | ---------------
338 | df : pandas.DataFrame
339 |
340 | Returns
341 | ---------------
342 | str
343 | HTML table for display.
344 | """
345 | classes = ['table', 'table-hover']
346 | html_raw = '
11 | The postgresql.conf comparison tool was created to 12 | make it easier to track changes to the database's main configuration file. 13 | These configuration options, referred to by the Pg hackers as 14 | GUCs 15 | ( 16 | Grand Unified Configuration), 17 | do change over time with major versions. These changes typically reflect new features added, existing features 18 | improved, and other changes. 19 |
20 | 21 |22 | This project is open source and available on 23 | GitHub. 24 |
25 | 26 |27 | 28 | This blog post 29 | illustrates how this tool can be used to research 30 | and prepare for upgrading PostgreSQL major versions. 31 | 32 |
33 |41 | Configuration data is available for Postgres 10 and newer. 42 | Configurations for new versions will be added as they are released. 43 |
44 |45 | NOTE: This tool previously included Postgres versions 9.2 through 9.6. 46 | Those versions were not included during the rewrite from using 47 | `postgresql.conf` directly to using the more robust comparison 48 | from `pg_catalog.pg_settings` data. 49 |
50 |10 | The compare your Configuration feature has been removed. 11 | To compare your Postgres configuration against defaults for your version 12 | explore the data returned by the following query. 13 |
14 | 15 |16 | SELECT * 17 | FROM pg_catalog.pg_settings 18 | WHERE boot_val != setting 19 | ; 20 |21 | 22 |
10 | Select a parameter to view the defaults across versions of PostgreSQL. 11 |
12 | 13 |
10 | Select two versions of Postgres to see the differences between their
11 | postgresql.conf
parameters and defaults. Parameters that remain
12 | the same in both versions are not displayed.
13 |