├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── apps
├── __init__.py
└── schema
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── filters.py
│ ├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
│ ├── models.py
│ ├── tables.py
│ └── views.py
├── configs
├── newrelic.ini
├── nginx.conf
├── setup.sh
├── supervisor.conf
└── uwsgi.ini
├── dbhub
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── common.py
│ └── dev.py
├── urls.py
└── wsgi.py
├── manage.py
├── requirements.txt
├── runtests.sh
├── screenshoot.png
├── scripts
├── __init__.py
├── check.py
├── db.py
├── parser.py
└── sync.py
├── templates
└── columns.html
├── test-requirements.txt
└── tests
├── __init__.py
└── test_parser.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | !static/.gitkeep
3 | static/
4 | env/
5 | db.sqlite3
6 | prod.py
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | env:
3 | - DJANGO=1.11
4 | python:
5 | - "2.7"
6 | # command to install dependencies
7 | install:
8 | - pip install -r test-requirements.txt
9 | # command to run tests
10 | script: ./runtests.sh
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 会分期
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 | # DBHub
2 |
3 | DBHub is a free database schema automatic discovery and easily commenting tool.
4 | DBHub now can support mysql, mongodb and other 6 databases, it is easy to extend.
5 | DBHub has a web page for viewing all schema with comments and has an admin system for managing.
6 |
7 | [](https://travis-ci.org/huifenqi/dbhub)
8 |
9 | 
10 |
11 | ## Structure:
12 |
13 | * apps (web and admin page)
14 | * scripts (sync and check)
15 | * configs (supervisor, newrelic and uwsgi)
16 |
17 | ## Installation
18 |
19 | To set up a development environment quickly, install Python 2.x first. It
20 | comes with virtualenv built-in. so create a virtual environment with:
21 |
22 | `virtualenv -p python2 env`
23 |
24 | Install dependencies:
25 |
26 | `pip install -r requirements.txt`
27 |
28 | ## Create default user
29 |
30 | `python manage.py createsuperuser`
31 |
32 | ## Run server
33 |
34 | `python manage.py runserver --settings=dbhub.settings.dev`
35 |
36 | ## Add database
37 |
38 | * name: database name
39 | * config: whole url for connect with database
40 | * for MySQL: mysql://{username}:{password}@{database-url}:3306/{database-name}?charset=utf8
41 | * for SQLite: sqlite:////{absolute-path-to-db-file}
42 | * for MongoDB: mongodb://{username}:{password}@{database-url}:27017/{database-name}
43 |
44 | ## Sync databases' schema and check columns' enumeration
45 |
46 | `python manage.py runscript sync`
47 |
48 | `python manage.py runscript check --script--args [db_name] [table_name] `
49 |
50 | ## How to write comments with enumeration
51 |
52 | 1. write description first;
53 | 2. write enumerations below with {enum}: {description}.
54 |
55 | ```
56 | charset with description, blah, blah, blah
57 |
58 | utf8: A UTF-8 encoding of the Unicode character set using one to three bytes per character. default utf8 of mysql, max length is 3 bytes, not support characters, such as emoji.
59 |
60 | utf8mb4: A UTF-8 encoding of the Unicode character set using one to four bytes per character.
61 |
62 | ```
63 |
64 | ## Supported dialects
65 |
66 | * MySQL
67 | * MongoDB
68 | * PostgreSQL
69 | * Oracle
70 | * SQLite
71 | * Microsoft SQL Server
72 | * Firebird
73 | * Sybase
74 |
--------------------------------------------------------------------------------
/apps/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/apps/__init__.py
--------------------------------------------------------------------------------
/apps/schema/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/apps/schema/__init__.py
--------------------------------------------------------------------------------
/apps/schema/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.forms import Textarea
3 | from django.db import models
4 | from reversion.admin import VersionAdmin
5 |
6 | from models import Database, Table, Column, Index
7 |
8 |
9 | class DatabaseAdmin(VersionAdmin):
10 | list_display = ('name', 'config', 'engine', 'charset', 'comment', 'enable')
11 | list_editable = ('config', 'enable')
12 | readonly_fields = ('engine', 'charset')
13 |
14 |
15 | class TableAdmin(VersionAdmin):
16 | list_display = ('name', 'database', 'engine', 'charset', 'comment', 'is_deleted')
17 | search_fields = ('name', 'comment')
18 | readonly_fields = ('name', 'database', 'engine', 'charset')
19 | list_filter = ('database',)
20 | list_editable = ('comment',)
21 |
22 | formfield_overrides = {
23 | models.TextField: {'widget': Textarea(attrs={'rows': 3, 'cols': 40})},
24 | }
25 |
26 |
27 | class ColumnAdmin(VersionAdmin):
28 | list_display = ('name', 'table', 'data_type', 'is_null', 'default_value', 'comment', 'other_enums', 'is_enum',
29 | 'is_comment_dirty', 'is_deleted')
30 | search_fields = ('name', 'table__name', 'comment')
31 | readonly_fields = ('name', 'table', 'data_type', 'is_null', 'default_value', 'other_enums')
32 | list_filter = ('is_comment_dirty', 'table')
33 | list_editable = ('comment', 'is_enum', 'is_deleted', 'is_comment_dirty')
34 |
35 | formfield_overrides = {
36 | models.TextField: {'widget': Textarea(attrs={'rows': 3, 'cols': 40})},
37 | }
38 |
39 |
40 | class IndexAdmin(VersionAdmin):
41 | list_display = ('name', 'table', 'type', 'include_columns')
42 | search_fields = ('name', 'table__name')
43 | readonly_fields = ('name', 'table', 'type', 'include_columns')
44 |
45 |
46 | admin.site.register(Database, DatabaseAdmin)
47 | admin.site.register(Table, TableAdmin)
48 | admin.site.register(Column, ColumnAdmin)
49 | admin.site.register(Index, IndexAdmin)
50 |
--------------------------------------------------------------------------------
/apps/schema/apps.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from django.apps import AppConfig
4 |
5 |
6 | class SchemaConfig(AppConfig):
7 | name = 'schema'
8 |
--------------------------------------------------------------------------------
/apps/schema/filters.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.forms import TextInput
3 | from django_filters import FilterSet, CharFilter, ModelChoiceFilter
4 |
5 | from .models import Database, Table, Column
6 |
7 |
8 | class ColumnFilter(FilterSet):
9 | database = ModelChoiceFilter(queryset=Database.objects.filter(enable=True), method='database_filter', label='',
10 | empty_label='Choose a database')
11 | table = ModelChoiceFilter(queryset=Table.objects.all(), label='')
12 | word = CharFilter(method='word_filter', label='',
13 | widget=TextInput(attrs={'placeholder': 'Search a word'}))
14 |
15 | def database_filter(self, queryset, name, value):
16 | return queryset.filter(table__database=value)
17 |
18 | def word_filter(self, queryset, name, value):
19 | return queryset.filter(
20 | Q(name__icontains=value) | Q(comment__icontains=value) | Q(table__name__icontains=value) | Q(
21 | table__comment__icontains=value))
22 |
23 | class Meta:
24 | model = Column
25 | fields = ['database', 'table', 'word']
26 |
--------------------------------------------------------------------------------
/apps/schema/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.14 on 2020-09-27 15:21
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Column',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('name', models.CharField(help_text='\u5217\u540d', max_length=100)),
22 | ('data_type', models.CharField(blank=True, help_text='\u6570\u636e\u7c7b\u578b', max_length=100, null=True)),
23 | ('is_null', models.NullBooleanField(choices=[(True, 'NULL'), (False, 'NOT NULL')], help_text='\u53ef\u7a7a')),
24 | ('default_value', models.CharField(blank=True, help_text='\u9ed8\u8ba4\u503c', max_length=100, null=True)),
25 | ('comment', models.TextField(blank=True, help_text='\u6ce8\u91ca', max_length=5000, null=True)),
26 | ('is_comment_dirty', models.BooleanField(default=False)),
27 | ('is_enum', models.BooleanField(default=False)),
28 | ('other_enums', models.TextField(blank=True, default='', help_text='\u672a\u5339\u914d\u7684\u679a\u4e3e\u503c', max_length=2000, null=True)),
29 | ('is_deleted', models.BooleanField(default=False)),
30 | ],
31 | ),
32 | migrations.CreateModel(
33 | name='Database',
34 | fields=[
35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36 | ('name', models.CharField(help_text='\u6570\u636e\u5e93\u540d', max_length=100, unique=True)),
37 | ('config', models.CharField(help_text='\u914d\u7f6e', max_length=100, unique=True)),
38 | ('engine', models.CharField(blank=True, default='InnoDB', help_text='\u5f15\u64ce', max_length=10, null=True)),
39 | ('charset', models.CharField(blank=True, default='utf8', help_text='\u7f16\u7801', max_length=100, null=True)),
40 | ('comment', models.TextField(blank=True, default='TBD', help_text='\u6ce8\u91ca', max_length=5000, null=True)),
41 | ('enable', models.NullBooleanField(choices=[(True, 'on'), (False, 'off')], default=True, help_text='\u662f\u5426\u542f\u7528')),
42 | ],
43 | ),
44 | migrations.CreateModel(
45 | name='Index',
46 | fields=[
47 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
48 | ('name', models.CharField(help_text='\u7d22\u5f15\u540d', max_length=100)),
49 | ('type', models.CharField(blank=True, choices=[('KEY', 'KEY'), ('PRIMARY KEY', 'PRIMARY KEY'), ('UNIQUE KEY', 'UNIQUE KEY')], help_text='\u7c7b\u578b', max_length=100, null=True)),
50 | ('include_columns', models.CharField(blank=True, help_text='\u5305\u542b\u5b57\u6bb5', max_length=100, null=True)),
51 | ],
52 | ),
53 | migrations.CreateModel(
54 | name='Table',
55 | fields=[
56 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
57 | ('name', models.CharField(help_text='\u8868\u540d', max_length=100)),
58 | ('engine', models.CharField(blank=True, help_text='\u5f15\u64ce', max_length=10, null=True)),
59 | ('charset', models.CharField(blank=True, help_text='\u7f16\u7801', max_length=100, null=True)),
60 | ('comment', models.TextField(blank=True, help_text='\u6ce8\u91ca', max_length=5000, null=True)),
61 | ('is_deleted', models.BooleanField(default=False)),
62 | ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='schema.Database')),
63 | ],
64 | ),
65 | migrations.AddField(
66 | model_name='index',
67 | name='table',
68 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='schema.Table'),
69 | ),
70 | migrations.AddField(
71 | model_name='column',
72 | name='table',
73 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='schema.Table'),
74 | ),
75 | migrations.AlterUniqueTogether(
76 | name='table',
77 | unique_together=set([('database', 'name')]),
78 | ),
79 | migrations.AlterUniqueTogether(
80 | name='index',
81 | unique_together=set([('table', 'name')]),
82 | ),
83 | migrations.AlterUniqueTogether(
84 | name='column',
85 | unique_together=set([('table', 'name')]),
86 | ),
87 | ]
88 |
--------------------------------------------------------------------------------
/apps/schema/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/apps/schema/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/schema/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals
4 |
5 | from django.db import models
6 |
7 |
8 | class Database(models.Model):
9 | NULL_TYPES = (
10 | (True, 'on'),
11 | (False, 'off'),
12 | )
13 | name = models.CharField(unique=True, max_length=100, help_text=u'数据库名')
14 | config = models.CharField(unique=True, max_length=100, help_text=u'配置')
15 | engine = models.CharField(max_length=10, help_text=u'引擎', default='InnoDB', null=True, blank=True)
16 | charset = models.CharField(max_length=100, help_text=u'编码', default='utf8', null=True, blank=True)
17 | comment = models.TextField(max_length=5000, help_text=u'注释', default='TBD', null=True, blank=True)
18 | enable = models.NullBooleanField(choices=NULL_TYPES, help_text=u'是否启用', default=True, null=True, blank=True)
19 |
20 | def __unicode__(self):
21 | return self.name
22 |
23 |
24 | class Table(models.Model):
25 | database = models.ForeignKey(Database)
26 | name = models.CharField(max_length=100, help_text=u'表名')
27 | engine = models.CharField(max_length=10, help_text=u'引擎', null=True, blank=True)
28 | charset = models.CharField(max_length=100, help_text=u'编码', null=True, blank=True)
29 | comment = models.TextField(max_length=5000, help_text=u'注释', null=True, blank=True)
30 | is_deleted = models.BooleanField(default=False)
31 |
32 | def __unicode__(self):
33 | return '{} ({})'.format(self.name, self.database.name)
34 |
35 | class Meta:
36 | unique_together = (('database', 'name'),)
37 |
38 |
39 | class Column(models.Model):
40 | NULL_TYPES = (
41 | (True, 'NULL'),
42 | (False, 'NOT NULL'),
43 | )
44 |
45 | table = models.ForeignKey(Table)
46 | name = models.CharField(max_length=100, help_text=u'列名')
47 | data_type = models.CharField(max_length=100, help_text=u'数据类型', null=True, blank=True)
48 | is_null = models.NullBooleanField(choices=NULL_TYPES, help_text=u'可空', null=True, blank=True)
49 | default_value = models.CharField(max_length=100, help_text=u'默认值', null=True, blank=True)
50 | comment = models.TextField(max_length=5000, help_text=u'注释', null=True, blank=True)
51 | is_comment_dirty = models.BooleanField(default=False)
52 | is_enum = models.BooleanField(default=False)
53 | other_enums = models.TextField(max_length=2000, help_text=u'未匹配的枚举值', default='', null=True, blank=True)
54 | is_deleted = models.BooleanField(default=False)
55 |
56 | def __unicode__(self):
57 | return self.name
58 |
59 | class Meta:
60 | unique_together = (('table', 'name'),)
61 |
62 |
63 | class Index(models.Model):
64 | KEY_TYPES = (
65 | ('KEY', 'KEY'),
66 | ('PRIMARY KEY', 'PRIMARY KEY'),
67 | ('UNIQUE KEY', 'UNIQUE KEY'),
68 | )
69 |
70 | table = models.ForeignKey(Table)
71 | name = models.CharField(max_length=100, help_text=u'索引名')
72 | type = models.CharField(max_length=100, choices=KEY_TYPES, help_text=u'类型', null=True, blank=True)
73 | include_columns = models.CharField(max_length=100, help_text=u'包含字段', null=True, blank=True)
74 |
75 | def __unicode__(self):
76 | return self.name
77 |
78 | class Meta:
79 | unique_together = (('table', 'name'),)
80 |
--------------------------------------------------------------------------------
/apps/schema/tables.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 | import django_tables2 as tables
4 |
5 | from models import Column
6 |
7 |
8 | TEMPLATE = """
9 | {% if record.is_deleted %}Deleted{% endif %}
10 | {% if record.is_comment_dirty %}
11 | {% if record.other_enums|length > 20 %}
12 | Not Match:
{{record.other_enums|slice:":20"}}...
13 | {% else %}
14 | Not Match:
{{record.other_enums}}{% endif %}{% endif %}
15 | """
16 |
17 |
18 | class ColumnTable(tables.Table):
19 | table_comment = tables.TemplateColumn('{{ value|linebreaks }}', verbose_name='Table Comment',
20 | accessor='table.comment')
21 | comment = tables.TemplateColumn('{{ value|linebreaks }}')
22 | warning_info = tables.TemplateColumn(TEMPLATE, verbose_name='Warning', orderable=False)
23 | database = tables.TemplateColumn('{{value}}', accessor='table.database')
24 |
25 | class Meta:
26 | model = Column
27 | sequence = ('name', 'table', 'data_type', 'is_null', 'default_value', 'comment', 'table_comment',
28 | 'warning_info')
29 | template_name = "django_tables2/semantic.html"
30 | exclude = ("id", "is_comment_dirty", "is_enum", "is_deleted", "other_enums")
31 |
--------------------------------------------------------------------------------
/apps/schema/views.py:
--------------------------------------------------------------------------------
1 | from django_filters.views import FilterView
2 | from django_tables2 import SingleTableMixin
3 | from django.conf import settings
4 | from dal import autocomplete
5 |
6 | from .tables import ColumnTable
7 | from .models import Column, Table
8 | from .filters import ColumnFilter
9 |
10 |
11 | class ColumnListView(SingleTableMixin, FilterView):
12 | table_class = ColumnTable
13 | model = Column
14 | template_name = "columns.html"
15 | filterset_class = ColumnFilter
16 |
17 | def get_queryset(self):
18 | return super(ColumnListView, self).get_queryset().select_related("table")
19 |
20 | def get_context_data(self, **kwargs):
21 | ctx = super(ColumnListView, self).get_context_data(**kwargs)
22 | ctx['title'] = settings.TITLE
23 | ctx['enable_oauth'] = settings.ENABLE_OAUTH
24 | return ctx
25 |
26 |
27 | class TableAutocomplete(autocomplete.Select2QuerySetView):
28 | def dispatch(self, request, *args, **kwargs):
29 | self.database = request.GET.get('database', None)
30 | return super(TableAutocomplete, self).dispatch(request, *args, **kwargs)
31 |
32 | def get_queryset(self):
33 | qs = Table.objects.all()
34 | if self.database:
35 | qs = qs.filter(database=self.database)
36 | if self.q:
37 | qs = qs.filter(name__contains=self.q)
38 | return qs
39 |
--------------------------------------------------------------------------------
/configs/newrelic.ini:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------------
2 |
3 | #
4 | # This file configures the New Relic Python Agent.
5 | #
6 | # The path to the configuration file should be supplied to the function
7 | # newrelic.agent.initialize() when the agent is being initialized.
8 | #
9 | # The configuration file follows a structure similar to what you would
10 | # find for Microsoft Windows INI files. For further information on the
11 | # configuration file format see the Python ConfigParser documentation at:
12 | #
13 | # http://docs.python.org/library/configparser.html
14 | #
15 | # For further discussion on the behaviour of the Python agent that can
16 | # be configured via this configuration file see:
17 | #
18 | # http://newrelic.com/docs/python/python-agent-configuration
19 | #
20 |
21 | # ---------------------------------------------------------------------------
22 |
23 | # Here are the settings that are common to all environments.
24 |
25 | [newrelic]
26 |
27 | # You must specify the license key associated with your New
28 | # Relic account. This key binds the Python Agent's data to your
29 | # account in the New Relic service.
30 | license_key = eba6291ea8bd00cfa3729c86bb943097ab34d532
31 |
32 | # The application name. Set this to be the name of your
33 | # application as you would like it to show up in New Relic UI.
34 | # The UI will then auto-map instances of your application into a
35 | # entry on your home dashboard page.
36 | app_name = dbhub
37 |
38 | # When "true", the agent collects performance data about your
39 | # application and reports this data to the New Relic UI at
40 | # newrelic.com. This global switch is normally overridden for
41 | # each environment below.
42 | monitor_mode = true
43 |
44 | # Sets the name of a file to log agent messages to. Useful for
45 | # debugging any issues with the agent. This is not set by
46 | # default as it is not known in advance what user your web
47 | # application processes will run as and where they have
48 | # permission to write to. Whatever you set this to you must
49 | # ensure that the permissions for the containing directory and
50 | # the file itself are correct, and that the user that your web
51 | # application runs as can write to the file. If not able to
52 | # write out a log file, it is also possible to say "stderr" and
53 | # output to standard error output. This would normally result in
54 | # output appearing in your web server log.
55 | #log_file = /tmp/newrelic-python-agent.log
56 |
57 | # Sets the level of detail of messages sent to the log file, if
58 | # a log file location has been provided. Possible values, in
59 | # increasing order of detail, are: "critical", "error", "warning",
60 | # "info" and "debug". When reporting any agent issues to New
61 | # Relic technical support, the most useful setting for the
62 | # support engineers is "debug". However, this can generate a lot
63 | # of information very quickly, so it is best not to keep the
64 | # agent at this level for longer than it takes to reproduce the
65 | # problem you are experiencing.
66 | log_level = info
67 |
68 | # The Python Agent communicates with the New Relic service using
69 | # SSL by default. Note that this does result in an increase in
70 | # CPU overhead, over and above what would occur for a non SSL
71 | # connection, to perform the encryption involved in the SSL
72 | # communication. This work is though done in a distinct thread
73 | # to those handling your web requests, so it should not impact
74 | # response times. You can if you wish revert to using a non SSL
75 | # connection, but this will result in information being sent
76 | # over a plain socket connection and will not be as secure.
77 | ssl = true
78 |
79 | # High Security Mode enforces certain security settings, and
80 | # prevents them from being overridden, so that no sensitive data
81 | # is sent to New Relic. Enabling High Security Mode means that
82 | # SSL is turned on, request parameters are not collected, and SQL
83 | # can not be sent to New Relic in its raw form. To activate High
84 | # Security Mode, it must be set to 'true' in this local .ini
85 | # configuration file AND be set to 'true' in the server-side
86 | # configuration in the New Relic user interface. For details, see
87 | # https://docs.newrelic.com/docs/subscriptions/high-security
88 | high_security = false
89 |
90 | # The Python Agent will attempt to connect directly to the New
91 | # Relic service. If there is an intermediate firewall between
92 | # your host and the New Relic service that requires you to use a
93 | # HTTP proxy, then you should set both the "proxy_host" and
94 | # "proxy_port" settings to the required values for the HTTP
95 | # proxy. The "proxy_user" and "proxy_pass" settings should
96 | # additionally be set if proxy authentication is implemented by
97 | # the HTTP proxy. The "proxy_scheme" setting dictates what
98 | # protocol scheme is used in talking to the HTTP proxy. This
99 | # would normally always be set as "http" which will result in the
100 | # agent then using a SSL tunnel through the HTTP proxy for end to
101 | # end encryption.
102 | # proxy_scheme = http
103 | # proxy_host = hostname
104 | # proxy_port = 8080
105 | # proxy_user =
106 | # proxy_pass =
107 |
108 | # Capturing request parameters is off by default. To enable the
109 | # capturing of request parameters, first ensure that the setting
110 | # "attributes.enabled" is set to "true" (the default value), and
111 | # then add "request.parameters.*" to the "attributes.include"
112 | # setting. For details about attributes configuration, please
113 | # consult the documentation.
114 | # attributes.include = request.parameters.*
115 |
116 | # The transaction tracer captures deep information about slow
117 | # transactions and sends this to the UI on a periodic basis. The
118 | # transaction tracer is enabled by default. Set this to "false"
119 | # to turn it off.
120 | transaction_tracer.enabled = true
121 |
122 | # Threshold in seconds for when to collect a transaction trace.
123 | # When the response time of a controller action exceeds this
124 | # threshold, a transaction trace will be recorded and sent to
125 | # the UI. Valid values are any positive float value, or (default)
126 | # "apdex_f", which will use the threshold for a dissatisfying
127 | # Apdex controller action - four times the Apdex T value.
128 | transaction_tracer.transaction_threshold = apdex_f
129 |
130 | # When the transaction tracer is on, SQL statements can
131 | # optionally be recorded. The recorder has three modes, "off"
132 | # which sends no SQL, "raw" which sends the SQL statement in its
133 | # original form, and "obfuscated", which strips out numeric and
134 | # string literals.
135 | transaction_tracer.record_sql = obfuscated
136 |
137 | # Threshold in seconds for when to collect stack trace for a SQL
138 | # call. In other words, when SQL statements exceed this
139 | # threshold, then capture and send to the UI the current stack
140 | # trace. This is helpful for pinpointing where long SQL calls
141 | # originate from in an application.
142 | transaction_tracer.stack_trace_threshold = 0.5
143 |
144 | # Determines whether the agent will capture query plans for slow
145 | # SQL queries. Only supported in MySQL and PostgreSQL. Set this
146 | # to "false" to turn it off.
147 | transaction_tracer.explain_enabled = true
148 |
149 | # Threshold for query execution time below which query plans
150 | # will not not be captured. Relevant only when "explain_enabled"
151 | # is true.
152 | transaction_tracer.explain_threshold = 0.5
153 |
154 | # Space separated list of function or method names in form
155 | # 'module:function' or 'module:class.function' for which
156 | # additional function timing instrumentation will be added.
157 | transaction_tracer.function_trace =
158 |
159 | # The error collector captures information about uncaught
160 | # exceptions or logged exceptions and sends them to UI for
161 | # viewing. The error collector is enabled by default. Set this
162 | # to "false" to turn it off.
163 | error_collector.enabled = true
164 |
165 | # To stop specific errors from reporting to the UI, set this to
166 | # a space separated list of the Python exception type names to
167 | # ignore. The exception name should be of the form 'module:class'.
168 | error_collector.ignore_errors =
169 |
170 | # Browser monitoring is the Real User Monitoring feature of the UI.
171 | # For those Python web frameworks that are supported, this
172 | # setting enables the auto-insertion of the browser monitoring
173 | # JavaScript fragments.
174 | browser_monitoring.auto_instrument = true
175 |
176 | # A thread profiling session can be scheduled via the UI when
177 | # this option is enabled. The thread profiler will periodically
178 | # capture a snapshot of the call stack for each active thread in
179 | # the application to construct a statistically representative
180 | # call tree.
181 | thread_profiler.enabled = true
182 |
183 | # ---------------------------------------------------------------------------
184 |
185 | #
186 | # The application environments. These are specific settings which
187 | # override the common environment settings. The settings related to a
188 | # specific environment will be used when the environment argument to the
189 | # newrelic.agent.initialize() function has been defined to be either
190 | # "development", "test", "staging" or "production".
191 | #
192 |
193 | [newrelic:development]
194 | monitor_mode = false
195 |
196 | [newrelic:test]
197 | monitor_mode = false
198 |
199 | [newrelic:staging]
200 | app_name = Python Application (Staging)
201 | monitor_mode = true
202 |
203 | [newrelic:production]
204 | monitor_mode = true
205 |
206 | # ---------------------------------------------------------------------------
207 |
--------------------------------------------------------------------------------
/configs/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name dbhub.wanggezhi.com;
4 |
5 | location /static {
6 | alias /data/www/dbhub/static;
7 | }
8 |
9 | location / {
10 | proxy_pass http://127.0.0.1:8005;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/configs/setup.sh:
--------------------------------------------------------------------------------
1 | ln -s /data/www/dbhub/configs/supervisor.conf /etc/supervisor/conf.d/dbhub.conf
2 | ln -s /data/www/dbhub/configs/nginx.conf /etc/nginx/conf.d/dbhub.conf
3 |
--------------------------------------------------------------------------------
/configs/supervisor.conf:
--------------------------------------------------------------------------------
1 | [program:dbhub]
2 | environment = NEW_RELIC_CONFIG_FILE=/data/www/%(program_name)s/configs/newrelic.ini
3 | directory = /data/www/%(program_name)s/configs
4 | command = /data/www/%(program_name)s/env/local/bin/uwsgi --ini uwsgi.ini
5 | autostart = true
6 | autorestart = true
7 | stopsignal = QUIT
8 | killasgroup = true
9 | buffer-size = 65535
10 | redirect_stderr = true
11 | stdout_logfile_maxbytes = 0
12 | stdout_logfile_backups = 0
13 | stdout_logfile = /data/logs/%(program_name)s/uwsgi.stdout.log
14 | stderr_logfile=/data/logs/%(program_name)s/uwsgi.stderr.log
15 |
--------------------------------------------------------------------------------
/configs/uwsgi.ini:
--------------------------------------------------------------------------------
1 | [uwsgi]
2 | http = 127.0.0.1:8005
3 | chdir = /data/www/dbhub/
4 | wsgi-file = dbhub/wsgi.py
5 | master = true
6 | processes = 2
7 | harakiri = 60
8 | limit-as = 1000
9 | max-requests = 5000
10 | single-interpreter = true
11 | enable-threads = true
12 | env = DJANGO_SETTINGS_MODULE=dbhub.settings.prod
13 | virtualenv = /data/www/dbhub/env/
14 |
--------------------------------------------------------------------------------
/dbhub/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/dbhub/__init__.py
--------------------------------------------------------------------------------
/dbhub/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/dbhub/settings/__init__.py
--------------------------------------------------------------------------------
/dbhub/settings/common.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Django settings for DBHub project.
4 |
5 | Generated by 'django-admin startproject' using Django 1.8.18.
6 |
7 | For more information on this file, see
8 | https://docs.djangoproject.com/en/1.8/topics/settings/
9 |
10 | For the full list of settings and their values, see
11 | https://docs.djangoproject.com/en/1.8/ref/settings/
12 | """
13 |
14 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
15 | import os
16 |
17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '7@$-=s^akdzw5$02*=q#9@2@7otk%d3u3j3cm7rt)mc=wg2g0!'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = ['*']
29 |
30 | # Application definition
31 |
32 | INSTALLED_APPS = (
33 | 'django.contrib.admin',
34 | 'django.contrib.auth',
35 | 'django.contrib.contenttypes',
36 | 'django.contrib.sessions',
37 | 'django.contrib.messages',
38 | 'django.contrib.staticfiles',
39 | 'django_extensions',
40 | "django_tables2",
41 | "django_filters",
42 | "dal",
43 | "dal_select2",
44 | "oauthadmin",
45 | "reversion",
46 | 'apps.schema',
47 | )
48 |
49 | MIDDLEWARE_CLASSES = (
50 | 'django.contrib.sessions.middleware.SessionMiddleware',
51 | 'oauthadmin.middleware.OauthAdminSessionMiddleware',
52 | 'django.middleware.common.CommonMiddleware',
53 | 'django.middleware.csrf.CsrfViewMiddleware',
54 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
55 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
56 | 'django.contrib.messages.middleware.MessageMiddleware',
57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
58 | 'django.middleware.security.SecurityMiddleware',
59 | )
60 |
61 | ROOT_URLCONF = 'dbhub.urls'
62 |
63 | TEMPLATES = [
64 | {
65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
66 | "DIRS": ["templates"],
67 | 'APP_DIRS': True,
68 | 'OPTIONS': {
69 | 'context_processors': [
70 | 'django.template.context_processors.debug',
71 | 'django.template.context_processors.request',
72 | 'django.contrib.auth.context_processors.auth',
73 | 'django.contrib.messages.context_processors.messages',
74 | ],
75 | },
76 | },
77 | ]
78 |
79 | WSGI_APPLICATION = 'dbhub.wsgi.application'
80 |
81 | # Database
82 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases
83 |
84 | DATABASES = {
85 | 'default': {
86 | 'ENGINE': 'django.db.backends.sqlite3',
87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
88 | }
89 | }
90 |
91 | # Internationalization
92 | # https://docs.djangoproject.com/en/1.8/topics/i18n/
93 |
94 | LANGUAGE_CODE = 'zh-hans'
95 |
96 | TIME_ZONE = 'Asia/Shanghai'
97 |
98 | USE_I18N = True
99 |
100 | USE_L10N = True
101 |
102 | USE_TZ = True
103 |
104 | # Static files (CSS, JavaScript, Images)
105 | # https://docs.djangoproject.com/en/1.8/howto/static-files/
106 |
107 | STATIC_URL = '/static/'
108 |
109 | STATIC_ROOT = BASE_DIR + '/../static/'
110 |
111 | TITLE = '[DEMO] DBHub\'s self design'
112 | DB_INSTANCES = []
113 |
114 | # django-admin-oauth2
115 | SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
116 |
117 | OAUTHADMIN_CLIENT_ID = ''
118 | OAUTHADMIN_CLIENT_SECRET = ''
119 | OAUTHADMIN_BASE_URL = "https://sso.huifenqi.com/sso/oauth/"
120 | OAUTHADMIN_AUTH_URL = 'https://sso.huifenqi.com/sso/authorize/'
121 | OAUTHADMIN_TOKEN_URL = 'https://sso.huifenqi.com/sso/token/'
122 | OAUTHADMIN_GROUPS = []
123 | OAUTHADMIN_SCOPE = []
124 |
125 | # replace admin login with oauth login
126 | ENABLE_OAUTH = False
127 |
--------------------------------------------------------------------------------
/dbhub/settings/dev.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .common import *
4 |
--------------------------------------------------------------------------------
/dbhub/urls.py:
--------------------------------------------------------------------------------
1 | """dbhub URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
14 | """
15 | from django.conf.urls import include, url
16 | from django.contrib import admin
17 | from django.contrib.auth.decorators import login_required
18 | from django.conf import settings
19 | from apps.schema.views import ColumnListView, TableAutocomplete
20 |
21 | admin.site.site_header = 'DBHub'
22 |
23 | urlpatterns = [
24 | url(r"^$", ColumnListView.as_view(), name="index"),
25 | url(r'^autocomplete/$', login_required(TableAutocomplete.as_view()), name='table-autocomplete'),
26 | ]
27 |
28 | if settings.ENABLE_OAUTH:
29 | old_admin_login = admin.site.login
30 | admin.site.login = login_required(admin.site.login)
31 | urlpatterns += [
32 | url(r'^admin-login/', old_admin_login),
33 | url(r'^oauth/', include('oauthadmin.urls')),
34 | ]
35 | settings.LOGIN_URL = '/oauth/login/'
36 | settings.LOGOUT_REDIRECT_URL = '/oauth/logout_redirect/'
37 |
38 | urlpatterns += [
39 | url(r'^admin/', include(admin.site.urls)),
40 | ]
41 |
--------------------------------------------------------------------------------
/dbhub/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for dbhub project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbhub.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbhub.settings.dev")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.11.29
2 | django-autocomplete-light==3.2.10
3 | django-extensions==2.1.0
4 | django-filter==1.1.0
5 | django-reversion==3.0.2
6 | django-tables2==1.21.2
7 | MySQL-python==1.2.5
8 | SQLAlchemy>=1.3.0
9 | sqlsoup>=0.9.1
10 | pymongo
11 | lark-parser
12 | uwsgi==2.0.17.1
13 | https://github.com/bastionhost/django-admin-oauth2/archive/master.zip
14 | https://github.com/pajachiet/pymongo-schema/archive/master.zip
15 |
--------------------------------------------------------------------------------
/runtests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | DJANGO_SETTINGS_MODULE=settings py.test $*
3 |
--------------------------------------------------------------------------------
/screenshoot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/screenshoot.png
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huifenqi/dbhub/825ce53f0b1c0e3c2e033ebe2c92bfcfbf51bd9e/scripts/__init__.py
--------------------------------------------------------------------------------
/scripts/check.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import pymongo
3 |
4 | from apps.schema.models import Database
5 | from scripts.db import DB
6 | from scripts.parser import CommentParser
7 |
8 |
9 | class Checker(object):
10 | def __init__(self, database, t_name_list):
11 | self.database = database
12 | self.dialect = database.config.split(':')[0]
13 | if self.dialect == 'mongodb':
14 | parts1 = self.database.config.rsplit(':', 1)
15 | parts2 = parts1[1].split('/')
16 | host = parts1[0]
17 | port = int(parts2[0])
18 | db_name = parts2[1]
19 | self.db = pymongo.MongoClient(host, port)[db_name]
20 | else:
21 | self.db = DB(self.database.config)
22 | if len(t_name_list) == 1 and t_name_list[0] == '':
23 | tables = database.table_set.all()
24 | else:
25 | tables = database.table_set.filter(name__in=t_name_list)
26 | if not tables:
27 | tables = database.table_set.all()
28 | self.tables = tables
29 |
30 | def get_enum_list(self, table_name, column_name):
31 | real_enums = []
32 | if self.dialect == 'mongodb':
33 | pipeline = [
34 | {
35 | u"$group": {
36 | u"_id": {
37 | column_name: u"${}".format(column_name)
38 | },
39 | u"COUNT(*)": {
40 | u"$sum": 1
41 | }
42 | }
43 | },
44 | {
45 | u"$project": {
46 | column_name: u"$_id.{}".format(column_name),
47 | u"COUNT(*)": u"$COUNT(*)",
48 | u"_id": 0
49 | }
50 | }
51 | ]
52 | cursor = self.db[table_name].aggregate(pipeline, allowDiskUse=True)
53 | real_enums = [r[column_name] for r in cursor]
54 | else:
55 | tb = getattr(self.db, table_name)
56 | enum_list = tb.group_by(column_name).all()
57 | for row in enum_list:
58 | tmp = getattr(row, column_name)
59 | if isinstance(tmp, unicode):
60 | real_enums.append(tmp.encode('utf-8'))
61 | else:
62 | real_enums.append(str(tmp))
63 | return real_enums
64 |
65 | def run(self):
66 | for table in self.tables:
67 | for column in table.column_set.all():
68 | # skip column which is dirty
69 | if column.is_comment_dirty or column.is_deleted:
70 | continue
71 | comment_enums = CommentParser.get_enums((column.comment or '').encode('utf-8'))
72 | # set is_enum as have comment_enums
73 | if comment_enums and not column.is_enum:
74 | column.is_enum = True
75 | # skip column which is not enum
76 | if not column.is_enum:
77 | continue
78 | real_enums = self.get_enum_list(table.name, column.name)
79 | if len(real_enums) > 50:
80 | column.other_enums = u'枚举值异常!'
81 | column.is_comment_dirty = True
82 | column.save()
83 | continue
84 | not_match_enums = (set(real_enums) - set(comment_enums))
85 | if not_match_enums:
86 | print(self.database, table, column, comment_enums, real_enums)
87 | column.is_comment_dirty = True
88 | warning = ','.join(not_match_enums)
89 | column.other_enums = warning
90 | else:
91 | column.is_comment_dirty = False
92 | column.save()
93 |
94 |
95 | def run(db_list, t_list):
96 | db_name_list = db_list.split(',')
97 | if len(db_name_list) == 1 and db_name_list[0] == '':
98 | databases = Database.objects.filter(enable=True)
99 | else:
100 | databases = Database.objects.filter(enable=True, name__in=db_name_list)
101 | t_name_list = t_list.split(',')
102 | for database in databases:
103 | checker = Checker(database, t_name_list)
104 | checker.run()
105 |
--------------------------------------------------------------------------------
/scripts/db.py:
--------------------------------------------------------------------------------
1 | import sqlsoup
2 | from sqlalchemy import create_engine
3 | from sqlalchemy.orm import scoped_session, sessionmaker
4 |
5 |
6 | class Engine(object):
7 | def __new__(cls, connect_url):
8 | engine = create_engine(connect_url,
9 | strategy='threadlocal',
10 | pool_size=5,
11 | pool_recycle=1800,
12 | encoding='utf-8',
13 | max_overflow=2)
14 | return engine
15 |
16 |
17 | class DB(object):
18 | def __new__(cls, connect_url):
19 | return sqlsoup.SQLSoup(Engine(connect_url), session=scoped_session(sessionmaker(
20 | autoflush=False,
21 | expire_on_commit=False,
22 | autocommit=True)))
23 |
24 |
25 | if __name__ == '__main__':
26 | # db = DB('mysql://root:123456@localhost:3306/test?charset=utf8')
27 | pass
28 |
--------------------------------------------------------------------------------
/scripts/parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import re
3 |
4 |
5 | class CommentParser(object):
6 | @classmethod
7 | def parse(cls, comment):
8 | pattern = r"([-\d\w]+)([:|:|-]{1})\s*[^::,,-]?"
9 | objs = re.findall(pattern, comment, re.M | re.I)
10 | return objs
11 |
12 | @classmethod
13 | def parse_with_lark(cls, comment):
14 | from lark import Lark
15 | parser = Lark('''start: WORD "," WORD "!"
16 | LCASE_LETTER: "a".."z"
17 | UCASE_LETTER: "A".."Z"
18 | CN_ZH_LETTER: /[u"\u4e00-\u9fa5"]/
19 | LETTER: UCASE_LETTER | LCASE_LETTER | CN_ZH_LETTER
20 | WORD: LETTER+
21 | %import common.NUMBER // imports from terminal library
22 | %ignore " " // Disregard spaces in text
23 | ''', parser='lalr')
24 |
25 | print(parser.parse(comment).pretty())
26 | # print(parser.parse(u'默认代扣银行卡 1:默认代扣 0:不默认代扣'))
27 | # not works as parsing library need accurate sentence
28 |
29 | @classmethod
30 | def get_enums(cls, comment):
31 | enums = list(set([obj[0] for obj in cls.parse(comment)]))
32 | return sorted(enums)
33 |
--------------------------------------------------------------------------------
/scripts/sync.py:
--------------------------------------------------------------------------------
1 | import pymongo
2 | from django.conf import settings
3 | from sqlalchemy import create_engine, inspect
4 | from sqlalchemy import MetaData
5 | from pymongo_schema.extract import extract_pymongo_client_schema
6 | from pymongo_schema.tosql import mongo_schema_to_mapping
7 |
8 | from apps.schema.models import Database, Table, Column, Index
9 |
10 |
11 | class RelationalDBSync(object):
12 | @staticmethod
13 | def save_indexes(t, indexes):
14 | for index in indexes:
15 | i, created = Index.objects.get_or_create(table=t, name=index.name)
16 | i.type = 'UNIQUE KEY' if index.unique else 'KEY'
17 | i.include_columns = ', '.join([c.name for c in index.columns])
18 | i.save()
19 |
20 | @staticmethod
21 | def save_primary_keys(t, primary_keys):
22 | for index in primary_keys:
23 | i, created = Index.objects.get_or_create(table=t, name=index.name)
24 | i.type = 'PRIMARY KEY'
25 | i.include_columns = index.name
26 | i.save()
27 |
28 | @staticmethod
29 | def save_columns(t, columns):
30 | for column in columns:
31 | default_value = column.server_default.arg if column.server_default else None
32 | c, created = Column.objects.get_or_create(table=t, name=column.name)
33 | try:
34 | c.data_type = str(column.type).split(' ')[0]
35 | except Exception:
36 | c.data_type = repr(column.type)
37 | c.is_null = column.nullable
38 | c.default_value = default_value
39 | if not c.comment and column.comment:
40 | c.comment = column.comment
41 | c.save()
42 |
43 | def build(self, database):
44 | engine = create_engine(database.config)
45 | m = MetaData()
46 | m.reflect(engine)
47 | if not database.charset:
48 | # fill database info
49 | database.charset = engine.dialect.encoding
50 | database.save()
51 | for table in m.sorted_tables:
52 | print(table.name)
53 | dialect = database.config.split(':')[0]
54 | table_info = table.dialect_options[dialect]._non_defaults
55 | t, created = Table.objects.get_or_create(database=database, name=table.name)
56 | t.engine = table_info.get('engine', '')
57 | t.charset = table_info.get('default charset', '')
58 | if not t.comment and table.comment:
59 | t.comment = table.comment
60 | t.save()
61 | self.save_columns(t, table.columns)
62 | self.save_primary_keys(t, table.primary_key.columns)
63 | self.save_indexes(t, table.indexes)
64 |
65 |
66 | class MongoDBSync(object):
67 | def __init__(self, database):
68 | self.database = database
69 | parts1 = database.config.rsplit(':', 1)
70 | parts2 = parts1[1].split('/')
71 | self.host = parts1[0]
72 | self.port = int(parts2[0])
73 | self.db = parts2[1]
74 |
75 | def build(self):
76 | with pymongo.MongoClient(self.host, self.port) as client:
77 | for collection in client[self.db].list_collection_names():
78 | print(collection)
79 | schema = extract_pymongo_client_schema(client, [self.db], [collection])
80 | mapping = mongo_schema_to_mapping(schema)
81 | t, created = Table.objects.get_or_create(database=self.database, name=collection)
82 | if self.db not in mapping or collection not in mapping[self.db]:
83 | continue
84 | for column in mapping[self.db][collection].keys():
85 | if column == 'pk':
86 | continue
87 | c, created = Column.objects.get_or_create(table=t, name=column)
88 | c.data_type = mapping[self.db][collection][column]['type']
89 | c.is_null = True
90 | c.save()
91 |
92 |
93 | def init_databases():
94 | for instance in settings.DB_INSTANCES:
95 | engine = create_engine(instance)
96 | insp = inspect(engine)
97 | db_list = insp.get_schema_names()
98 | dbs = set(db_list) - {'information_schema', 'performance_schema', 'mysql', 'sys'}
99 | for db in dbs:
100 | config = '{}/{}?charset=utf8'.format(instance.rstrip('/'), db)
101 | d, created = Database.objects.get_or_create(name=db)
102 | d.config = config
103 | d.save()
104 |
105 |
106 | def run():
107 | init_databases()
108 | databases = Database.objects.filter(enable=True)
109 | for database in databases:
110 | if database.config.startswith('mongodb'):
111 | MongoDBSync(database).build()
112 | else:
113 | RelationalDBSync().build(database)
114 |
115 |
116 | if __name__ == '__main__':
117 | run()
118 |
--------------------------------------------------------------------------------
/templates/columns.html:
--------------------------------------------------------------------------------
1 | {% load render_table from django_tables2 %}
2 |
3 |
4 |