fias 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +142 -10
- data/fias.gemspec +3 -0
- data/lib/fias.rb +5 -2
- data/lib/fias/active_record/address_object.rb +170 -15
- data/lib/fias/active_record/address_object_type.rb +8 -1
- data/lib/fias/dbf_wrapper.rb +90 -0
- data/lib/fias/importer.rb +30 -0
- data/lib/fias/importer/base.rb +59 -0
- data/lib/fias/importer/pg.rb +81 -0
- data/lib/fias/importer/sqlite.rb +38 -0
- data/lib/fias/version.rb +1 -1
- data/lib/generators/fias/migration.rb +8 -4
- data/tasks/fias.rake +23 -26
- metadata +57 -6
- data/lib/fias/import/dbf_wrapper.rb +0 -83
- data/lib/fias/import/pg.rb +0 -144
data/README.md
CHANGED
@@ -16,33 +16,66 @@ gem 'fias', git: 'https://github.com/evilmartians/kladr.git'
|
|
16
16
|
|
17
17
|
2. Скачать и распаковать базу ФИАС в tmp/fias (по умолчанию)
|
18
18
|
|
19
|
-
#
|
19
|
+
# Импорт структуры данных
|
20
20
|
|
21
21
|
Возможны два варианта:
|
22
22
|
|
23
23
|
```
|
24
|
-
rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=...]
|
24
|
+
rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=... ONLY=...]
|
25
25
|
```
|
26
26
|
|
27
27
|
Либо:
|
28
28
|
|
29
29
|
```
|
30
|
-
rails g fias:migration [--path=... --prefix=...]
|
30
|
+
rails g fias:migration [--path=... --prefix=... --only=...]
|
31
31
|
```
|
32
32
|
|
33
33
|
Первый вариант - для использования гема вне рельсов, либо для случая, когда
|
34
34
|
актуальная база ФИАС будет использоваться только для локального мапинга, и
|
35
35
|
на продакшн попасть не должна.
|
36
36
|
|
37
|
+
В параметре only можно передать имена нужных таблиц, houses - все
|
38
|
+
таблицы домов.
|
39
|
+
|
37
40
|
# Импорт данных
|
38
41
|
|
39
42
|
```
|
40
|
-
rake fias:import
|
43
|
+
rake fias:import PREFIX=fias PATH=tmp/fias ONLY=address_objects
|
44
|
+
rake fias:import PREFIX=fias PATH=tmp/fias ONLY=houses
|
41
45
|
```
|
42
46
|
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
Первый пример импортирует только адресные объекты (без справочников),
|
48
|
+
второй - только дома.
|
49
|
+
|
50
|
+
Поддерживается импорт в Postgres и SQLite (нужно для :memory: баз)
|
51
|
+
|
52
|
+
# Импорт данных в память (для рейк тасок)
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
ActiveRecord::Base.configurations['fias'] = {
|
56
|
+
:adapter => 'sqlite3',
|
57
|
+
:database => ':memory:'
|
58
|
+
}
|
59
|
+
Fias::AddressObject.establish_connection :fias
|
60
|
+
|
61
|
+
fias = Fias::DbfWrapper.new('tmp/fias')
|
62
|
+
importer = Fias::Importer.build(
|
63
|
+
adapter: 'sqlite3', connection: Fias::AddressObject.connection.raw_connection
|
64
|
+
)
|
65
|
+
tables = fias.tables(:address_objects)
|
66
|
+
|
67
|
+
# Мигрейт. В таком виде так как стандартный мигратор всегда открывает новое
|
68
|
+
# соединение с БД, а в случае с SQLite это означает пересоздание базы :memory:.
|
69
|
+
Fias::AddressObject.connection.instance_exec do
|
70
|
+
eval(importer.schema(tables))
|
71
|
+
end
|
72
|
+
|
73
|
+
importer.import(tables) do |name, record, index|
|
74
|
+
record[:aclevel] == 1 # Только активные
|
75
|
+
end
|
76
|
+
|
77
|
+
Fias::AddressObject.count # И дальше импорт
|
78
|
+
```
|
46
79
|
|
47
80
|
# Некоторые замечания про ФИАС
|
48
81
|
|
@@ -55,24 +88,123 @@ rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
|
|
55
88
|
образования хранится в поле AOGUID. Из внешних таблиц логично ссылаться
|
56
89
|
на AOGUID, а не на AOID.
|
57
90
|
|
91
|
+
# Маппинг, он же итерирование
|
92
|
+
|
93
|
+
Задачи:
|
94
|
+
|
95
|
+
1. Сопоставить базу приложения с ФИАСом.
|
96
|
+
2. Уведомлять приложение об изменениях в связанных объектах ФИАСа.
|
97
|
+
3. Уведомлять приложения о новых данных в ФИАСе.
|
98
|
+
|
99
|
+
В таблице, с которой устанавливаются соответствия нужно создать поле для
|
100
|
+
хранения UUID записи ФИАСа.
|
101
|
+
|
102
|
+
На примере регионов:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# Будем искать соответствия регионов приложения регионам в ФИАС
|
106
|
+
scope = Region
|
107
|
+
fias_scope = Fias::AddressObject.leveled(:region)
|
108
|
+
|
109
|
+
# Обработчик событий итератора
|
110
|
+
matcher = ->(action, record, *objects) {
|
111
|
+
match = objects.first
|
112
|
+
|
113
|
+
case action
|
114
|
+
|
115
|
+
# Появилась новая запись в ФИАСе
|
116
|
+
when :created
|
117
|
+
puts "Region #{match.abbrevated}"
|
118
|
+
Region.create!(
|
119
|
+
fias_reference: match.id,
|
120
|
+
title: match.abbrevated
|
121
|
+
)
|
122
|
+
|
123
|
+
# Элемент в ФИАСе обновлен, нужно (?) обновить базу приложения
|
124
|
+
when :updated
|
125
|
+
record.update_attributes(
|
126
|
+
title: match.abbrevated,
|
127
|
+
fias_reference: match.id
|
128
|
+
)
|
129
|
+
puts "#{record.title} became #{match.abbrevated}"
|
130
|
+
|
131
|
+
# Объект в ФИАСе был, но из актуального состояния перешел в неактуальное.
|
132
|
+
# Типа, Припять: умерший город.
|
133
|
+
# Скорее всего, такой элемент в базе приложения нужно скрыть.
|
134
|
+
when :deleted
|
135
|
+
raise NotImplementedError, "#{record.title} #{objects.first.abbrevated}"
|
136
|
+
|
137
|
+
# Элемент в ФИАСе разделился: например, поселение Черто-Полохово
|
138
|
+
# было преобразовано в две деревни: Чертово и Полохово
|
139
|
+
# В этом случае параметр objects содержит объекты ФИАСа, на которые
|
140
|
+
# разделился старый.
|
141
|
+
when :split
|
142
|
+
raise NotImplementedError
|
143
|
+
|
144
|
+
# Элемент в ФИАСе объединился
|
145
|
+
# Типа, присоединили Московскую область к Москве
|
146
|
+
# В этом случае objects.first это запись ФИАС о новом объекте, а остальные
|
147
|
+
# элементы objects - записи ФИАС объединившихся объектов.
|
148
|
+
when :joined
|
149
|
+
raise NotImplementedError
|
150
|
+
|
151
|
+
# Элементу базы приложения нет соответствия в ФИАСе. Пытаемся сопоставить
|
152
|
+
# и зафиксировать, если найшли соответствующий вариант.
|
153
|
+
when :match
|
154
|
+
match = objects.detect { |b| b.actual? || b.name == record.title }
|
155
|
+
if match.present?
|
156
|
+
record.update_attributes(
|
157
|
+
title: match.abbrevated,
|
158
|
+
fias_reference: match.id
|
159
|
+
)
|
160
|
+
puts "#{record.title} became #{match.abbrevated} (new)"
|
161
|
+
else
|
162
|
+
puts "Record not found #{record.title}"
|
163
|
+
end
|
164
|
+
|
165
|
+
else
|
166
|
+
raise 'Unknown action!'
|
167
|
+
end
|
168
|
+
}
|
169
|
+
|
170
|
+
# Итерирует существующие в базе приложения элементы
|
171
|
+
# fias_reference - название колонки с UUID соответствующей региону записи
|
172
|
+
# ФИАСа
|
173
|
+
# title - название региона
|
174
|
+
fias_scope.match_existing(scope, :fias_reference, :title, &matcher)
|
175
|
+
|
176
|
+
# Итерирует отсутствующие в базе приложения, но имеющиеся в скоупе
|
177
|
+
# ФИАСа адреса.
|
178
|
+
fias_scope.actual.match_missing(scope, :fias_reference, &matcher)
|
179
|
+
```
|
180
|
+
|
181
|
+
Примечания:
|
182
|
+
|
183
|
+
1. В случае неоднозначного совпадения ФИАС и приложения, нужно ставить поиск
|
184
|
+
соответствия на модерацию.
|
185
|
+
2. В случае :split, :joined - тоже на модерацию. Хотя, разделения происходят
|
186
|
+
и нечасто, однако, в ФИАСе таких случаев больше полутора тысяч.
|
187
|
+
3. При начальном импорте данных из ФИАСа, #match_missing лучше начать вызывать
|
188
|
+
только после того как соответствия старой базы ФИАСу будут полностью разрулены.
|
189
|
+
|
58
190
|
# Работа с данными
|
59
191
|
|
60
192
|
Существующие регионы:
|
61
193
|
|
62
|
-
```
|
194
|
+
```ruby
|
63
195
|
Fias::AddressObject.actual.leveled(:region).all
|
64
196
|
```
|
65
197
|
|
66
198
|
Подчиненные объекты в регионе (области, районы, столица региона):
|
67
199
|
|
68
|
-
```
|
200
|
+
```ruby
|
69
201
|
region = Fias::AddressObject.actual.leveled(:region).first
|
70
202
|
region.children.actual
|
71
203
|
```
|
72
204
|
|
73
205
|
# TODO
|
74
206
|
|
75
|
-
1. Индексы.
|
207
|
+
1. Индексы. Индексы. ИНДЕКСЫ.
|
76
208
|
|
77
209
|
# Полезные ссылки
|
78
210
|
|
data/fias.gemspec
CHANGED
@@ -21,4 +21,7 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.add_dependency 'rake'
|
22
22
|
gem.add_dependency 'activerecord', '> 3'
|
23
23
|
gem.add_dependency 'progress_bar'
|
24
|
+
gem.add_development_dependency 'pg'
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
26
|
+
gem.add_development_dependency 'hirb'
|
24
27
|
end
|
data/lib/fias.rb
CHANGED
@@ -3,8 +3,11 @@ require 'dbf'
|
|
3
3
|
require 'active_record'
|
4
4
|
|
5
5
|
require 'fias/version'
|
6
|
-
require 'fias/
|
7
|
-
require 'fias/
|
6
|
+
require 'fias/dbf_wrapper'
|
7
|
+
require 'fias/importer'
|
8
|
+
require 'fias/importer/base'
|
9
|
+
require 'fias/importer/pg'
|
10
|
+
require 'fias/importer/sqlite'
|
8
11
|
|
9
12
|
require 'fias/railtie' if defined?(Rails)
|
10
13
|
|
@@ -1,9 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# TODO: Поделить на файлы
|
1
3
|
module Fias
|
2
4
|
class AddressObject < ActiveRecord::Base
|
3
|
-
|
5
|
+
# TODO: Тут надо понять как префикс передать
|
6
|
+
if defined?(Rails)
|
7
|
+
self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
|
8
|
+
else
|
9
|
+
self.table_name = "fias_address_objects"
|
10
|
+
end
|
11
|
+
|
4
12
|
self.primary_key = 'aoid'
|
5
13
|
|
6
14
|
alias_attribute :name, :formalname
|
15
|
+
alias_attribute :id, :aoid
|
7
16
|
|
8
17
|
# Родительские объекты (Ленобласть для Лодейнопольского района)
|
9
18
|
# Для "проезд 1-й Конной Лахты 2-й" - Санкт-Петербург и Ленинград.
|
@@ -20,17 +29,25 @@ module Fias
|
|
20
29
|
primary_key: 'aoguid',
|
21
30
|
foreign_key: 'parentguid'
|
22
31
|
|
23
|
-
#
|
24
|
-
|
32
|
+
# Предыдущие исторические версии названия (Ленинград для Питера)
|
33
|
+
# Может быть несколько, если произошло слияние
|
34
|
+
has_many :previous_versions,
|
25
35
|
class_name: 'AddressObject',
|
26
|
-
|
27
|
-
|
36
|
+
foreign_key: 'aoid',
|
37
|
+
primary_key: 'previd'
|
28
38
|
|
29
|
-
# Следующая
|
30
|
-
|
39
|
+
# Следующая исторические версии названия (Питер для Ленинграда)
|
40
|
+
# Может быть несколько, если произошло разделение
|
41
|
+
has_many :next_versions,
|
31
42
|
class_name: 'AddressObject',
|
32
|
-
|
33
|
-
|
43
|
+
foreign_key: 'aoid',
|
44
|
+
primary_key: 'nextid'
|
45
|
+
|
46
|
+
# Полное наименование типа объекта (город, улица)
|
47
|
+
belongs_to :address_object_type,
|
48
|
+
class_name: '::Fias::AddressObjectType',
|
49
|
+
foreign_key: 'shortname',
|
50
|
+
primary_key: 'scname'
|
34
51
|
|
35
52
|
# Актуальные записи (активные в настоящий момент)
|
36
53
|
# Проверено, что livestatus уже достаточен для идентификации
|
@@ -45,18 +62,33 @@ module Fias
|
|
45
62
|
}
|
46
63
|
|
47
64
|
scope :sorted, order('formalname ASC')
|
65
|
+
scope :with_types, includes(:address_object_type)
|
66
|
+
scope :matching, ->(name) {
|
67
|
+
scope = if self.connection.adapter_name
|
68
|
+
where(%{ "formalname" @@ ? OR ? @@ "formalname"}, name, name)
|
69
|
+
else
|
70
|
+
where('formalname LIKE ?', "%#{name}%")
|
71
|
+
end
|
48
72
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
73
|
+
# Первыми идут центральные и крупные поселения
|
74
|
+
scope.order('aolevel ASC, centstatus DESC')
|
75
|
+
}
|
76
|
+
|
77
|
+
scope :same_region, ->(address_object) {
|
78
|
+
where(regioncode: address_object.regioncode)
|
79
|
+
}
|
80
|
+
|
81
|
+
# Значимые поселения
|
82
|
+
scope :central, where('centstatus > 0')
|
54
83
|
|
55
|
-
# Есть ли исторические варианты записи?
|
56
84
|
def has_history?
|
57
85
|
previd.present?
|
58
86
|
end
|
59
87
|
|
88
|
+
def actual?
|
89
|
+
livestatus == 1
|
90
|
+
end
|
91
|
+
|
60
92
|
# Актуальный родитель. Для 1-го проезда 2-й Конной Лахты - только Питер.
|
61
93
|
def parent
|
62
94
|
parents.actual.first
|
@@ -66,11 +98,134 @@ module Fias
|
|
66
98
|
AOLEVELS.key(aolevel)
|
67
99
|
end
|
68
100
|
|
101
|
+
# Название с сокращением / полным наименованием города
|
102
|
+
# "г. Санкт-Петербург" / "город Санкт-Петербург"
|
103
|
+
#
|
104
|
+
# Для работы метода требуется загрузить таблицу :address_object_types
|
105
|
+
#
|
106
|
+
# Параметр - режим:
|
107
|
+
# :obviously - очевидный режим". В этом режиме "Дагестан" вместо
|
108
|
+
# "Республика Дагестан", "АО" вместо "Автономная область",
|
109
|
+
# но всегда "Краснодарский край"
|
110
|
+
# :short - дописываются "г." и "респ."
|
111
|
+
# :long - дописываются "город" и "Республика"
|
112
|
+
#
|
113
|
+
# Пример:
|
114
|
+
# .abbrevated(:obviously) # Тульская область, Хакасия, Еврейская Аобл.
|
115
|
+
# .abbrevated(:short) # Тульская область, Респ. Хакасия, Еврейская Аобл.
|
116
|
+
# .abbrevated(:long) # Тульская область, Республика Хакасия, Еврейская автономная область
|
117
|
+
def abbrevated(mode = :obviously)
|
118
|
+
return name if address_object_type.blank? || shortname.blank?
|
119
|
+
|
120
|
+
case aolevel_sym
|
121
|
+
when :region
|
122
|
+
# "Ханты-Мансийский Автономный округ - Югра"
|
123
|
+
return name if regioncode == '86'
|
124
|
+
|
125
|
+
ending = name[-2..-1]
|
126
|
+
|
127
|
+
# Если название кончается на -ая -ий - по правилам русского языка
|
128
|
+
# нужно дописать в конец "область", "край"
|
129
|
+
must_append = SHN_MUST_APPEND_TO_ENDINGS.include?(ending) ||
|
130
|
+
shortname == 'Чувашия'
|
131
|
+
|
132
|
+
must_abbrevate = must_append ||
|
133
|
+
shortname == 'Чувашия' ||
|
134
|
+
mode != :obviously
|
135
|
+
|
136
|
+
return name unless must_abbrevate
|
137
|
+
|
138
|
+
abbr = case mode
|
139
|
+
when :short
|
140
|
+
shortname
|
141
|
+
when :long
|
142
|
+
address_object_type.name
|
143
|
+
when :obviously
|
144
|
+
if SHN_LEAVE_SHORTS_INTACT.include?(shortname)
|
145
|
+
shortname
|
146
|
+
else
|
147
|
+
address_object_type.name
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Точка не ставится для АО, края, Чувашии и длинных названий
|
152
|
+
abbr = "#{abbr}." if mode == :short && not(shortname.in?(SHN_MUST_NOT_APPEND_DOT))
|
153
|
+
|
154
|
+
if not(must_append) && must_abbrevate
|
155
|
+
"#{abbr} #{name}"
|
156
|
+
else
|
157
|
+
# "Республика" => "республика", но "АО" остается
|
158
|
+
abbr = abbr.mb_chars.downcase if not(shortname.in?(SHN_LEAVE_SHORTS_INTACT))
|
159
|
+
"#{name} #{abbr}"
|
160
|
+
end
|
161
|
+
else
|
162
|
+
abbr = if mode == :long
|
163
|
+
address_object_type.try(:name)
|
164
|
+
else
|
165
|
+
shortname
|
166
|
+
end
|
167
|
+
"#{abbr} #{name}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class << self
|
172
|
+
# Подробное описание см. в README
|
173
|
+
# TODO: OPERSTATUS
|
174
|
+
def match_existing(scope, fias_key_accessor, title_field_accessor, &block)
|
175
|
+
scope.find_each do |record|
|
176
|
+
aoid = record.send(fias_key_accessor)
|
177
|
+
title = record.send(title_field_accessor)
|
178
|
+
|
179
|
+
if aoid.present?
|
180
|
+
match = scoped.find_by_aoid(aoid)
|
181
|
+
|
182
|
+
if match.present?
|
183
|
+
unless match.actual?
|
184
|
+
next_versions = match.next_versions
|
185
|
+
|
186
|
+
if next_versions.empty?
|
187
|
+
yield(:deleted, record, match)
|
188
|
+
elsif next_versions.count == 1
|
189
|
+
next_version = next_versions.first
|
190
|
+
previous_versions_of_current = next_version.previous_versions
|
191
|
+
|
192
|
+
if previous_versions_of_current.count == 1
|
193
|
+
yield(:updated, record, next_version)
|
194
|
+
else
|
195
|
+
yield(:joined, record, next_version, previous_versions_of_current)
|
196
|
+
end
|
197
|
+
elsif next_versions.count > 1
|
198
|
+
yield(:split, record, next_versions)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
else
|
203
|
+
matches = scoped.matching(title)
|
204
|
+
yield(:match, record, *matches)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def match_missing(scope, address_object_key_field, &block)
|
210
|
+
scoped.each do |address_object|
|
211
|
+
unless scope.where(address_object_key_field => address_object.aoid).exists?
|
212
|
+
yield(:created, nil, address_object)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
69
218
|
# Коды уровня адресного объекта
|
70
219
|
AOLEVELS = {
|
71
220
|
region: 1, autonomy: 2, district: 3, city: 4,
|
72
221
|
territory: 5, settlement: 6, street: 7,
|
73
222
|
additional_territory: 90, additional_territory_slave: 91
|
74
223
|
}
|
224
|
+
|
225
|
+
# Дописывать сокращения обязательно, "Самарская" выглядит странно,
|
226
|
+
# всегда должно быть "Самарская область", а "Дагестан" понятно и так.
|
227
|
+
SHN_MUST_APPEND_TO_ENDINGS = %w(ая ий)
|
228
|
+
SHN_MUST_NOT_APPEND_DOT = %w(край АО Чувашия) # Не дописывать точку к сокращению
|
229
|
+
SHN_LEAVE_SHORTS_INTACT = %w(АО Аобл Чувашия) # В очевидном режиме не разворачивать "АО" в "Автономная область"
|
75
230
|
end
|
76
231
|
end
|
@@ -1,8 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
module Fias
|
2
3
|
class AddressObjectType < ActiveRecord::Base
|
3
|
-
|
4
|
+
# TODO: Тут надо понять как префикс передать
|
5
|
+
if defined?(Rails)
|
6
|
+
self.table_name = "#{Rails.application.config.fias.prefix}_address_object_types"
|
7
|
+
else
|
8
|
+
self.table_name = "fias_address_object_types"
|
9
|
+
end
|
4
10
|
self.primary_key = 'scname'
|
5
11
|
|
6
12
|
alias_attribute :name, :socrname
|
13
|
+
alias_attribute :abbrevation, :scname
|
7
14
|
end
|
8
15
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Fias
|
2
|
+
# Класс для доступа к DBF-файлам ФИАС.
|
3
|
+
#
|
4
|
+
# Пример:
|
5
|
+
# wrapper = Fias::DbfWrapper.new('tmp/fias')
|
6
|
+
# wrapper.address_objects.record_count
|
7
|
+
# wrapper.address_objects.each { |record| record.attributes }
|
8
|
+
#
|
9
|
+
# TODO: Добавить в инишилайзер tables, чтобы при создании проверять
|
10
|
+
# их наличие на диске
|
11
|
+
class DbfWrapper
|
12
|
+
# Открывает DBF-файлы ФИАСа
|
13
|
+
def initialize(pathspec)
|
14
|
+
unless Dir.exists?(pathspec)
|
15
|
+
raise ArgumentError, 'FIAS database path does not exists'
|
16
|
+
end
|
17
|
+
self.pathspec = pathspec
|
18
|
+
|
19
|
+
DBF_ACCESSORS.each do |accessor, dbf_name|
|
20
|
+
filename = File.join(pathspec, dbf_name)
|
21
|
+
|
22
|
+
if File.exists?(filename)
|
23
|
+
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
24
|
+
send("#{accessor}=", dbf)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Возвращает хеш {аксессор dbf-таблицы => таблица}
|
30
|
+
# На входе массив названий таблиц (строки), без параметров - все
|
31
|
+
# Макрос houses вернет все таблицы домов
|
32
|
+
#
|
33
|
+
# Пример:
|
34
|
+
# wrapper = Fias::DbfWrapper.new('tmp/fias')
|
35
|
+
# wrapper.tables(%w(address_objects address_object_types houses))
|
36
|
+
# ... {
|
37
|
+
# ... address_objects: Dbf::Table(...),
|
38
|
+
# ... address_object_types: Dbf::Table(...),
|
39
|
+
# ... house1: Dbf::Table(...),
|
40
|
+
# ... ...
|
41
|
+
# ... house99: Dbf::Table(...)
|
42
|
+
# ... }
|
43
|
+
def tables(*only)
|
44
|
+
only = only.first if only.first.is_a?(Array)
|
45
|
+
only = only.map(&:to_s)
|
46
|
+
|
47
|
+
hash = DBF_ACCESSORS.keys.map do |accessor|
|
48
|
+
accessor_s = accessor.to_s
|
49
|
+
is_houses = only.include?('houses') && accessor_s.starts_with?('house')
|
50
|
+
|
51
|
+
if only.include?(accessor_s) || is_houses
|
52
|
+
[accessor, send(accessor)]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
Hash[*hash.compact.flatten]
|
56
|
+
end
|
57
|
+
|
58
|
+
# { house01: "HOUSE01"..house99: "HOUSE99" }
|
59
|
+
HOUSES_ACCESSORS = Hash[*(1..99).map { |n|
|
60
|
+
[("house%0.2d" % n).to_sym, "HOUSE%0.2d.dbf" % n]
|
61
|
+
}.flatten]
|
62
|
+
|
63
|
+
# Таблица соответствий аттрибутов класса DBF-файлам
|
64
|
+
DBF_ACCESSORS = {
|
65
|
+
address_object_types: 'SOCRBASE.DBF',
|
66
|
+
current_statuses: 'CURENTST.DBF',
|
67
|
+
actual_statuses: 'ACTSTAT.DBF',
|
68
|
+
operation_statuses: 'OPERSTAT.DBF',
|
69
|
+
center_statuses: 'CENTERST.DBF',
|
70
|
+
interval_statuses: 'INTVSTAT.DBF',
|
71
|
+
estate_statues: 'ESTSTAT.DBF',
|
72
|
+
structure_statuses: 'STRSTAT.DBF',
|
73
|
+
address_objects: 'ADDROBJ.DBF',
|
74
|
+
house_intervals: 'HOUSEINT.DBF',
|
75
|
+
landmarks: 'LANDMARK.DBF'
|
76
|
+
}.merge(
|
77
|
+
HOUSES_ACCESSORS
|
78
|
+
)
|
79
|
+
|
80
|
+
DEFAULT_ENCODING = Encoding::CP866
|
81
|
+
|
82
|
+
attr_reader *DBF_ACCESSORS.keys
|
83
|
+
attr_reader :houses
|
84
|
+
|
85
|
+
private
|
86
|
+
attr_accessor :pathspec
|
87
|
+
attr_writer *DBF_ACCESSORS.keys
|
88
|
+
attr_writer :houses
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Фактори-метод, возвращает ссылку на объект импортера
|
4
|
+
# Принимает параметры:
|
5
|
+
# :adapter - название адаптера
|
6
|
+
# :connection - прямое соединение с базой данных (connection.raw_connection)
|
7
|
+
def self.build(options = {})
|
8
|
+
adapter = options.delete(:adapter) ||
|
9
|
+
ActiveRecord::Base.connection_config[:adapter]
|
10
|
+
|
11
|
+
connection = options.delete(:connection) ||
|
12
|
+
ActiveRecord::Base.connection.raw_connection
|
13
|
+
|
14
|
+
case adapter
|
15
|
+
when 'postgresql'
|
16
|
+
Pg.new(
|
17
|
+
connection,
|
18
|
+
options
|
19
|
+
)
|
20
|
+
when 'sqlite3'
|
21
|
+
Sqlite.new(
|
22
|
+
connection,
|
23
|
+
options
|
24
|
+
)
|
25
|
+
else
|
26
|
+
raise 'Only postgres & sqlite supported now, fork'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
class Base
|
4
|
+
def initialize(connection, options = {})
|
5
|
+
self.prefix = options.delete(:prefix) || 'fias'
|
6
|
+
self.connection = connection
|
7
|
+
end
|
8
|
+
|
9
|
+
# На входе - хеш (имя таблицы => dbf)
|
10
|
+
def schema(tables)
|
11
|
+
"".tap do |s|
|
12
|
+
tables.each do |name, dbf|
|
13
|
+
if dbf.present?
|
14
|
+
s << schema_for(name, table_name(name), dbf)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema_for(name, table_name, dbf)
|
21
|
+
"".tap do |s|
|
22
|
+
s << %{create_table "#{table_name}", id: false do |t|\n}
|
23
|
+
s << schema_columns(name, dbf)
|
24
|
+
s << "end\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def import(tables, &block)
|
29
|
+
tables.each do |name, dbf|
|
30
|
+
if dbf
|
31
|
+
import_table(name, table_name(name), dbf, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def import_table(name, table_name, dbf, &block)
|
37
|
+
raise NotImplementedError, 'Implement this in concrete class'
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
attr_accessor :prefix, :connection
|
42
|
+
|
43
|
+
private
|
44
|
+
def table_name(name)
|
45
|
+
"#{prefix}_#{name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def schema_columns(accessor, table)
|
49
|
+
"".tap do |s|
|
50
|
+
table.columns.each do |column|
|
51
|
+
column_name = column.name.downcase
|
52
|
+
column_def = column.schema_definition
|
53
|
+
s << " t.column #{column_def}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Класс для импорта данных из ФИАС в PostgreSQL.
|
4
|
+
# Используется COPY FROM STDIN.
|
5
|
+
class Pg < Base
|
6
|
+
def schema_for(name, table_name, dbf)
|
7
|
+
super + alter_table_to_pg_uuids(name, table_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def alter_table_to_pg_uuids(name, table_name)
|
12
|
+
"".tap do |s|
|
13
|
+
columns = CONVERT_TO_UUID[name]
|
14
|
+
if columns.present?
|
15
|
+
columns.each do |column|
|
16
|
+
s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def import_table(name, table_name, dbf, &block)
|
23
|
+
fields = table_fields(dbf)
|
24
|
+
columns = table_columns(fields)
|
25
|
+
|
26
|
+
truncate_table(table_name)
|
27
|
+
copy_from_stdin(table_name, columns)
|
28
|
+
|
29
|
+
dbf.each_with_index do |record, index|
|
30
|
+
should_import = yield(name, record.attributes, index) if block_given?
|
31
|
+
|
32
|
+
unless should_import === false
|
33
|
+
data = record.to_a
|
34
|
+
put_data(data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
put_copy_end
|
39
|
+
end
|
40
|
+
|
41
|
+
def truncate_table(table_name)
|
42
|
+
connection.exec "TRUNCATE TABLE #{table_name};"
|
43
|
+
end
|
44
|
+
|
45
|
+
def table_fields(table)
|
46
|
+
table.columns.map(&:name).map(&:downcase)
|
47
|
+
end
|
48
|
+
|
49
|
+
def table_columns(fields)
|
50
|
+
fields.join(', ')
|
51
|
+
end
|
52
|
+
|
53
|
+
def copy_from_stdin(table_name, columns)
|
54
|
+
sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
|
55
|
+
connection.exec(sql)
|
56
|
+
end
|
57
|
+
|
58
|
+
def put_data(data)
|
59
|
+
data.map! { |item| item == "" ? '-nil-' : item }
|
60
|
+
line = data.join("\t") + "\n"
|
61
|
+
connection.put_copy_data(line)
|
62
|
+
end
|
63
|
+
|
64
|
+
def put_copy_end
|
65
|
+
connection.put_copy_end
|
66
|
+
|
67
|
+
while res = connection.get_result
|
68
|
+
result_status = res.res_status(res.result_status)
|
69
|
+
unless result_status == 'PGRES_COMMAND_OK'
|
70
|
+
raise "Import failure: #{result_status}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Эти поля нужно отконвертировать в тип UUID после создания
|
77
|
+
CONVERT_TO_UUID = {
|
78
|
+
address_objects: %w(aoguid aoid previd nextid parentguid)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Нужно для :memory: баз в первую очередь
|
4
|
+
class Sqlite < Base
|
5
|
+
def import_table(name, table_name, dbf, &block)
|
6
|
+
truncate_table(table_name)
|
7
|
+
|
8
|
+
qmarks = ['?'] * dbf.columns.keys.size
|
9
|
+
qmarks = qmarks.join(', ')
|
10
|
+
|
11
|
+
dbf.each_with_index do |record, index|
|
12
|
+
data = record.attributes
|
13
|
+
|
14
|
+
should_import = yield(name, data, index) if block_given?
|
15
|
+
|
16
|
+
unless should_import === false
|
17
|
+
columns = data.keys.join(', ')
|
18
|
+
|
19
|
+
values = data.values.map do |value|
|
20
|
+
if value.is_a?(Date)
|
21
|
+
value.to_s
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
connection.execute("INSERT INTO #{table_name} (#{columns}) VALUES (#{qmarks});", values)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def truncate_table(table_name)
|
34
|
+
connection.execute "DELETE FROM #{table_name} WHERE 1"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/fias/version.rb
CHANGED
@@ -4,14 +4,18 @@ module Fias
|
|
4
4
|
|
5
5
|
class_option :prefix, type: :string, default: :fias, desc: 'Table names prefix'
|
6
6
|
class_option :path, type: :string, default: 'tmp/fias', desc: 'Path to FIAS dbfs'
|
7
|
+
class_option :only, type: :string, default: '', desc: 'Only tables'
|
7
8
|
|
8
9
|
source_root File.expand_path("../templates", __FILE__)
|
9
10
|
|
10
11
|
def generate_migration
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
only = options.only.split(',').map(&:strip)
|
13
|
+
wrapper = Fias::DbfWrapper.new(options.path)
|
14
|
+
importer = Fias::Importer.build(prefix: options.prefix)
|
15
|
+
|
16
|
+
tables = wrapper.tables(only)
|
17
|
+
@schema = importer.schema(tables)
|
18
|
+
@schema.gsub!("\n", "\n ")
|
15
19
|
|
16
20
|
migration_template 'create_fias_tables.rb', 'db/migrate/create_fias_tables'
|
17
21
|
end
|
data/tasks/fias.rake
CHANGED
@@ -25,46 +25,43 @@ namespace :fias do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
fias_path = ENV['FIAS'] || 'tmp/fias'
|
28
|
-
wrapper = Fias::
|
29
|
-
importer =
|
28
|
+
wrapper = Fias::DbfWrapper.new(fias_path)
|
29
|
+
importer = Fias::Importer.build(prefix: ENV['PREFIX'])
|
30
30
|
|
31
31
|
yield(wrapper, importer)
|
32
32
|
end
|
33
|
+
|
34
|
+
def only
|
35
|
+
only = ENV['ONLY'].to_s.split(',').map(&:strip)
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
|
-
desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL)'
|
39
|
+
desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL, EXCLUDE or ONLY tables)'
|
36
40
|
task may_be_rails(:create_tables) do
|
37
41
|
within_connection do |wrapper, importer|
|
38
|
-
|
42
|
+
tables = wrapper.tables(only)
|
43
|
+
# TODO: Добавить во враппер tables, это убрать
|
44
|
+
raise "DBF file not found for: #{key}" if tables.keys.any? { |key| key.nil? }
|
45
|
+
ActiveRecord::Schema.define do
|
46
|
+
eval(importer.schema(tables))
|
47
|
+
end
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
total_record_count = wrapper.tables.sum { |accessor, dbf| dbf.record_count }
|
47
|
-
|
48
|
-
puts 'Importing FIAS data...'
|
51
|
+
desc 'Import FIAS data'
|
52
|
+
task may_be_rails(:import) do
|
53
|
+
within_connection do |wrapper, importer|
|
54
|
+
tables = wrapper.tables(only)
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
bar.increment!
|
53
|
-
end
|
56
|
+
total_record_count = tables.sum do |accessor, dbf|
|
57
|
+
dbf.present? ? dbf.record_count : 0
|
54
58
|
end
|
55
|
-
end
|
56
59
|
|
57
|
-
|
58
|
-
task may_be_rails(:houses) do
|
59
|
-
within_connection do |wrapper, importer|
|
60
|
-
total_record_count = wrapper.houses.sum { |region, dbf| dbf.record_count }
|
60
|
+
puts 'Importing FIAS data...'
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
bar
|
65
|
-
importer.import_houses do
|
66
|
-
bar.increment!
|
67
|
-
end
|
62
|
+
bar = ProgressBar.new(total_record_count)
|
63
|
+
importer.import(tables) do
|
64
|
+
bar.increment!
|
68
65
|
end
|
69
66
|
end
|
70
67
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fias
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dbf
|
@@ -75,6 +75,54 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: pg
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: sqlite3
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: hirb
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
78
126
|
description: Ruby wrapper to FIAS database
|
79
127
|
email:
|
80
128
|
- gzigzigzeo@evilmartians.com
|
@@ -91,8 +139,11 @@ files:
|
|
91
139
|
- lib/fias.rb
|
92
140
|
- lib/fias/active_record/address_object.rb
|
93
141
|
- lib/fias/active_record/address_object_type.rb
|
94
|
-
- lib/fias/
|
95
|
-
- lib/fias/
|
142
|
+
- lib/fias/dbf_wrapper.rb
|
143
|
+
- lib/fias/importer.rb
|
144
|
+
- lib/fias/importer/base.rb
|
145
|
+
- lib/fias/importer/pg.rb
|
146
|
+
- lib/fias/importer/sqlite.rb
|
96
147
|
- lib/fias/railtie.rb
|
97
148
|
- lib/fias/version.rb
|
98
149
|
- lib/generators/fias/migration.rb
|
@@ -112,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
163
|
version: '0'
|
113
164
|
segments:
|
114
165
|
- 0
|
115
|
-
hash:
|
166
|
+
hash: 493497803985021140
|
116
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
168
|
none: false
|
118
169
|
requirements:
|
@@ -121,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
172
|
version: '0'
|
122
173
|
segments:
|
123
174
|
- 0
|
124
|
-
hash:
|
175
|
+
hash: 493497803985021140
|
125
176
|
requirements: []
|
126
177
|
rubyforge_project:
|
127
178
|
rubygems_version: 1.8.24
|
@@ -1,83 +0,0 @@
|
|
1
|
-
module Fias
|
2
|
-
module Import
|
3
|
-
# Класс для доступа к DBF-файлам ФИАС
|
4
|
-
# Пример:
|
5
|
-
# wrapper = DbfWrapper.new('tmp/fias')
|
6
|
-
# wrapper.address_objects.record_count
|
7
|
-
# wrapper.address_objects.each { |record| record.attributes }
|
8
|
-
class DbfWrapper
|
9
|
-
def initialize(pathspec)
|
10
|
-
unless Dir.exists?(pathspec)
|
11
|
-
raise ArgumentError, 'FIAS database path does not exists'
|
12
|
-
end
|
13
|
-
self.pathspec = pathspec
|
14
|
-
|
15
|
-
TABLES_ACCESSORS.each do |key, dbf_name|
|
16
|
-
filename = File.join(pathspec, dbf_name)
|
17
|
-
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
18
|
-
|
19
|
-
send("#{key}=", dbf)
|
20
|
-
end
|
21
|
-
|
22
|
-
self.houses = {}
|
23
|
-
Dir[File.join(pathspec, HOUSE_DBF_MASK)].each do |filename|
|
24
|
-
File.basename(filename) =~ /(\d+)/
|
25
|
-
region_code = $1.to_i
|
26
|
-
|
27
|
-
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
28
|
-
self.houses[region_code] = dbf
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def tables
|
33
|
-
hash = TABLES_ACCESSORS.keys.map do |accessor|
|
34
|
-
[accessor, send(accessor)]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def build_importer(options)
|
39
|
-
config = ActiveRecord::Base.connection_config
|
40
|
-
if config[:adapter] == 'postgresql'
|
41
|
-
import = Fias::Import::PgImporter.new(
|
42
|
-
ActiveRecord::Base.connection.raw_connection,
|
43
|
-
self,
|
44
|
-
options
|
45
|
-
)
|
46
|
-
else
|
47
|
-
raise 'Only postgres supported now, fork'
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Таблица соответствий аттрибутов класса DBF-файлам
|
52
|
-
TABLES_ACCESSORS = {
|
53
|
-
address_object_types: 'SOCRBASE.DBF',
|
54
|
-
current_statuses: 'CURENTST.DBF',
|
55
|
-
actual_statuses: 'ACTSTAT.DBF',
|
56
|
-
operation_statuses: 'OPERSTAT.DBF',
|
57
|
-
center_statuses: 'CENTERST.DBF',
|
58
|
-
interval_statuses: 'INTVSTAT.DBF',
|
59
|
-
estate_statues: 'ESTSTAT.DBF',
|
60
|
-
structure_statuses: 'STRSTAT.DBF',
|
61
|
-
address_objects: 'ADDROBJ.DBF',
|
62
|
-
house_intervals: 'HOUSEINT.DBF',
|
63
|
-
landmarks: 'LANDMARK.DBF'
|
64
|
-
}
|
65
|
-
HOUSE_DBF_MASK = 'HOUSE??.DBF'
|
66
|
-
|
67
|
-
# Эти поля нужно отконвертировать в тип UUID после создания
|
68
|
-
CONVERT_TO_UUID = {
|
69
|
-
address_objects: %w(aoguid aoid previd nextid parentguid)
|
70
|
-
}
|
71
|
-
|
72
|
-
DEFAULT_ENCODING = Encoding::CP866
|
73
|
-
|
74
|
-
attr_reader *TABLES_ACCESSORS.keys
|
75
|
-
attr_reader :houses
|
76
|
-
|
77
|
-
private
|
78
|
-
attr_accessor :pathspec
|
79
|
-
attr_writer *TABLES_ACCESSORS.keys
|
80
|
-
attr_writer :houses
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
data/lib/fias/import/pg.rb
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
module Fias
|
2
|
-
module Import
|
3
|
-
class PgImporter
|
4
|
-
# Класс для импорта данных из ФИАС в PostgreSQL.
|
5
|
-
# Используется COPY FROM STDIN.
|
6
|
-
def initialize(raw_connection, wrapper, options = {})
|
7
|
-
self.prefix = options.delete(:prefix) || 'fias'
|
8
|
-
self.raw_connection = raw_connection
|
9
|
-
self.wrapper = wrapper
|
10
|
-
end
|
11
|
-
|
12
|
-
# Генерирует схему базы для ActiveRecord в виде строки
|
13
|
-
def schema
|
14
|
-
"".tap do |s|
|
15
|
-
wrapper.tables.each do |accessor, table|
|
16
|
-
s << %{create_table "#{table_name(accessor)}", id: false do |t|\n}
|
17
|
-
s << dump_schema_for(accessor, table)
|
18
|
-
s << "end\n"
|
19
|
-
|
20
|
-
s << alter_table_to_pg_uuids(accessor)
|
21
|
-
end
|
22
|
-
|
23
|
-
s << %{create_table "#{table_name('houses')}", id: false do |t|\n}
|
24
|
-
s << dump_schema_for(:houses, wrapper.houses.values.first)
|
25
|
-
s << " t.column :regioncode, :integer\n"
|
26
|
-
s << "end\n"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def import(&block)
|
31
|
-
wrapper.tables.each do |accessor, table|
|
32
|
-
import_table(accessor, table, &block)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def import_houses(&block)
|
37
|
-
wrapper.houses.each do |region, table|
|
38
|
-
import_houses_for_region(region, table, &block)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
def dump_schema_for(accessor, table)
|
44
|
-
"".tap do |s|
|
45
|
-
table.columns.each do |column|
|
46
|
-
column_name = column.name.downcase
|
47
|
-
column_def = column.schema_definition
|
48
|
-
s << " t.column #{column_def}"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def alter_table_to_pg_uuids(accessor)
|
54
|
-
"".tap do |s|
|
55
|
-
columns = wrapper.class::CONVERT_TO_UUID[accessor]
|
56
|
-
if columns.present?
|
57
|
-
columns.each do |column|
|
58
|
-
s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name(accessor)} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def import_table(accessor, table, &block)
|
65
|
-
fields = table_fields(table)
|
66
|
-
columns = table_columns(fields)
|
67
|
-
|
68
|
-
tn = table_name(accessor)
|
69
|
-
|
70
|
-
truncate_table(tn)
|
71
|
-
copy_from_stdin(tn, columns)
|
72
|
-
|
73
|
-
table.each_with_index do |record|
|
74
|
-
data = record.to_a
|
75
|
-
put_data(data)
|
76
|
-
yield(accessor, data) if block_given?
|
77
|
-
end
|
78
|
-
|
79
|
-
put_copy_end
|
80
|
-
end
|
81
|
-
|
82
|
-
def import_houses_for_region(region, table, &block)
|
83
|
-
fields = table_fields(table)
|
84
|
-
fields << :regioncode
|
85
|
-
columns = table_columns(fields)
|
86
|
-
|
87
|
-
tn = table_name('houses')
|
88
|
-
|
89
|
-
truncate_table(tn)
|
90
|
-
copy_from_stdin(tn, columns)
|
91
|
-
|
92
|
-
table.each_with_index do |record|
|
93
|
-
data = record.to_a
|
94
|
-
data << region
|
95
|
-
put_data(data)
|
96
|
-
yield('houses', data) if block_given?
|
97
|
-
end
|
98
|
-
|
99
|
-
put_copy_end
|
100
|
-
end
|
101
|
-
|
102
|
-
def table_name(table)
|
103
|
-
"#{prefix}_#{table}"
|
104
|
-
end
|
105
|
-
|
106
|
-
def table_fields(table)
|
107
|
-
table.columns.map(&:name).map(&:downcase)
|
108
|
-
end
|
109
|
-
|
110
|
-
def table_columns(fields)
|
111
|
-
fields.join(',')
|
112
|
-
end
|
113
|
-
|
114
|
-
def truncate_table(table_name)
|
115
|
-
raw_connection.exec "TRUNCATE TABLE #{table_name};"
|
116
|
-
end
|
117
|
-
|
118
|
-
def copy_from_stdin(table_name, columns)
|
119
|
-
sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
|
120
|
-
raw_connection.exec(sql)
|
121
|
-
end
|
122
|
-
|
123
|
-
def put_data(data)
|
124
|
-
data.map! { |item| item == "" ? '-nil-' : item }
|
125
|
-
line = data.join("\t") + "\n"
|
126
|
-
raw_connection.put_copy_data(line)
|
127
|
-
end
|
128
|
-
|
129
|
-
def put_copy_end
|
130
|
-
raw_connection.put_copy_end
|
131
|
-
|
132
|
-
while res = raw_connection.get_result
|
133
|
-
result_status = res.res_status(res.result_status)
|
134
|
-
unless result_status == 'PGRES_COMMAND_OK'
|
135
|
-
raise "Import failure: #{result_status}"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
attr_accessor :raw_connection, :wrapper
|
141
|
-
attr_accessor :prefix, :table_mappings
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|