1 |
1942
|
aaronmk
|
#!/usr/bin/env python
|
2 |
|
|
# Loads a command's CSV output stream into a PostgreSQL table.
|
3 |
4446
|
aaronmk
|
# When no command is specified, just cleans up the specified table.
|
4 |
1942
|
aaronmk
|
# The command may be run more than once.
|
5 |
|
|
|
6 |
|
|
import csv
|
7 |
|
|
import os.path
|
8 |
1963
|
aaronmk
|
import re
|
9 |
1942
|
aaronmk
|
import subprocess
|
10 |
|
|
import sys
|
11 |
|
|
|
12 |
|
|
sys.path.append(os.path.dirname(__file__)+"/../lib")
|
13 |
|
|
|
14 |
|
|
import csvs
|
15 |
|
|
import exc
|
16 |
|
|
import opts
|
17 |
|
|
import sql
|
18 |
3080
|
aaronmk
|
import sql_io
|
19 |
2680
|
aaronmk
|
import sql_gen
|
20 |
1942
|
aaronmk
|
import streams
|
21 |
1963
|
aaronmk
|
import strings
|
22 |
1965
|
aaronmk
|
import util
|
23 |
1942
|
aaronmk
|
|
24 |
|
|
def main():
|
25 |
|
|
# Usage
|
26 |
|
|
env_names = []
|
27 |
|
|
def usage_err():
|
28 |
|
|
raise SystemExit('Usage: '+opts.env_usage(env_names)+' '+sys.argv[0]
|
29 |
4446
|
aaronmk
|
+' [input_cmd args...]')
|
30 |
1942
|
aaronmk
|
|
31 |
|
|
# Parse args
|
32 |
|
|
input_cmd = sys.argv[1:]
|
33 |
|
|
|
34 |
|
|
# Get config from env vars
|
35 |
|
|
table = opts.get_env_var('table', None, env_names)
|
36 |
|
|
schema = opts.get_env_var('schema', 'public', env_names)
|
37 |
4257
|
aaronmk
|
has_row_num = opts.env_flag('has_row_num', True, env_names)
|
38 |
1942
|
aaronmk
|
db_config = opts.get_env_vars(sql.db_config_names, None, env_names)
|
39 |
3271
|
aaronmk
|
verbosity = util.cast(float, opts.get_env_var('verbosity', 3, env_names))
|
40 |
1942
|
aaronmk
|
|
41 |
4446
|
aaronmk
|
if not (table != None and 'engine' in db_config): usage_err()
|
42 |
2890
|
aaronmk
|
|
43 |
1942
|
aaronmk
|
# Connect to DB
|
44 |
2680
|
aaronmk
|
def log(msg, level=1):
|
45 |
|
|
'''Higher level -> more verbose'''
|
46 |
3610
|
aaronmk
|
if level <= verbosity:
|
47 |
|
|
sys.stderr.write(strings.to_raw_str(msg.rstrip('\n')+'\n'))
|
48 |
2680
|
aaronmk
|
db = sql.connect(db_config, log_debug=log)
|
49 |
1942
|
aaronmk
|
|
50 |
3138
|
aaronmk
|
table = sql_gen.Table(table, schema)
|
51 |
|
|
|
52 |
1963
|
aaronmk
|
use_copy_from = [True]
|
53 |
|
|
|
54 |
|
|
# Loads data into the table using the currently-selected approach.
|
55 |
2680
|
aaronmk
|
def load():
|
56 |
1942
|
aaronmk
|
# Open input stream
|
57 |
|
|
proc = subprocess.Popen(input_cmd, stdout=subprocess.PIPE, bufsize=-1)
|
58 |
|
|
in_ = proc.stdout
|
59 |
|
|
|
60 |
|
|
# Get format info
|
61 |
|
|
info = csvs.stream_info(in_, parse_header=True)
|
62 |
|
|
dialect = info.dialect
|
63 |
1963
|
aaronmk
|
if csvs.is_tsv(dialect): use_copy_from[0] = False
|
64 |
3609
|
aaronmk
|
col_names = map(strings.to_unicode, info.header)
|
65 |
2680
|
aaronmk
|
for i, col in enumerate(col_names): # replace empty column names
|
66 |
|
|
if col == '': col_names[i] = 'column_'+str(i)
|
67 |
1942
|
aaronmk
|
|
68 |
1963
|
aaronmk
|
# Select schema and escape names
|
69 |
3608
|
aaronmk
|
def esc_name(name): return db.esc_name(name)
|
70 |
1942
|
aaronmk
|
|
71 |
4257
|
aaronmk
|
typed_cols = [sql_gen.TypedCol(v, 'text') for v in col_names]
|
72 |
|
|
if has_row_num:
|
73 |
|
|
typed_cols.insert(0, sql_gen.TypedCol('row_num', 'serial',
|
74 |
|
|
nullable=False))
|
75 |
2759
|
aaronmk
|
|
76 |
3149
|
aaronmk
|
log('Creating table')
|
77 |
4257
|
aaronmk
|
sql.create_table(db, table, typed_cols, has_pkey=has_row_num,
|
78 |
|
|
col_indexes=False)
|
79 |
3149
|
aaronmk
|
|
80 |
|
|
# Remove rows from any failed COPY FROM
|
81 |
|
|
sql.truncate(db, table)
|
82 |
|
|
|
83 |
2680
|
aaronmk
|
def load_():
|
84 |
|
|
# Create COPY FROM statement
|
85 |
|
|
if use_copy_from[0]:
|
86 |
3139
|
aaronmk
|
copy_from = ('COPY '+table.to_str(db)+' ('
|
87 |
2680
|
aaronmk
|
+(', '.join(map(esc_name, col_names)))
|
88 |
3578
|
aaronmk
|
+') FROM STDIN DELIMITER '+db.esc_value(dialect.delimiter)
|
89 |
|
|
+' NULL '+db.esc_value(''))
|
90 |
2680
|
aaronmk
|
assert not csvs.is_tsv(dialect)
|
91 |
|
|
copy_from += ' CSV'
|
92 |
|
|
if dialect.quoting != csv.QUOTE_NONE:
|
93 |
3578
|
aaronmk
|
quote_str = db.esc_value(dialect.quotechar)
|
94 |
|
|
copy_from += ' QUOTE '+quote_str
|
95 |
|
|
if dialect.doublequote: copy_from += ' ESCAPE '+quote_str
|
96 |
2680
|
aaronmk
|
copy_from += ';\n'
|
97 |
|
|
|
98 |
|
|
# Load the data
|
99 |
2892
|
aaronmk
|
line_in = streams.ProgressInputStream(in_, sys.stderr, n=1000)
|
100 |
2680
|
aaronmk
|
try:
|
101 |
|
|
if use_copy_from[0]:
|
102 |
|
|
log('Using COPY FROM')
|
103 |
|
|
log(copy_from, level=2)
|
104 |
|
|
db.db.cursor().copy_expert(copy_from, line_in)
|
105 |
|
|
else:
|
106 |
|
|
log('Using INSERT')
|
107 |
|
|
cols_ct = len(col_names)+1 # +1 for row_num
|
108 |
|
|
for row in csvs.make_reader(line_in, dialect):
|
109 |
|
|
row = map(strings.to_unicode, row)
|
110 |
|
|
row.insert(0, sql.default) # row_num is autogen
|
111 |
|
|
util.list_set_length(row, cols_ct) # truncate extra cols
|
112 |
3270
|
aaronmk
|
sql.insert(db, table, row, cacheable=False, log_level=5)
|
113 |
2680
|
aaronmk
|
finally:
|
114 |
|
|
line_in.close() # also closes proc.stdout
|
115 |
|
|
proc.wait()
|
116 |
|
|
sql.with_savepoint(db, load_)
|
117 |
1942
|
aaronmk
|
|
118 |
4446
|
aaronmk
|
if input_cmd != []:
|
119 |
|
|
try: load()
|
120 |
|
|
except sql.DatabaseErrors, e:
|
121 |
|
|
if use_copy_from[0]: # first try
|
122 |
|
|
exc.print_ex(e, plain=True)
|
123 |
|
|
use_copy_from[0] = False
|
124 |
|
|
load() # try again with different approach
|
125 |
|
|
else: raise
|
126 |
|
|
|
127 |
|
|
log('Cleaning up table')
|
128 |
|
|
sql_io.cleanup_table(db, table)
|
129 |
|
|
|
130 |
|
|
log('Vacuuming and reanalyzing table')
|
131 |
|
|
sql.vacuum(db, table)
|
132 |
1942
|
aaronmk
|
|
133 |
|
|
main()
|