Как вы избегаете строк для имен таблиц / столбцов SQLite в Python? - PullRequest
47 голосов
/ 29 июня 2011

Стандартный подход для использования значений переменных в запросах SQLite - это «стиль вопросительного знака», например:

import sqlite3
with sqlite3.connect(":memory:") as connection:
    connection.execute("CREATE TABLE foo(bar)")
    connection.execute("INSERT INTO foo(bar) VALUES (?)", ("cow",))

    print(list(connection.execute("SELECT * from foo")))
    # prints [(u'cow',)]

Однако это работает только для подстановки значений в запросы. Ошибка при использовании для имен таблиц или столбцов:

import sqlite3
with sqlite3.connect(":memory:") as connection:
    connection.execute("CREATE TABLE foo(?)", ("bar",))
    # raises sqlite3.OperationalError: near "?": syntax error

Ни в модуле sqlite3, ни в PEP 249 не упоминается функция для экранирования имен или значений. Предположительно, это отговаривает пользователей собирать свои запросы со строками, но это оставляет меня в растерянности.

Какая функция или метод наиболее подходят для использования имен переменных для столбцов или таблиц в SQLite? Я бы предпочел сделать это без каких-либо других зависимостей, поскольку я буду использовать его в своей собственной оболочке.

Я искал, но не смог найти четкого и полного описания соответствующей части синтаксиса SQLite, чтобы использовать его для написания своей собственной функции. Я хочу быть уверен, что это будет работать для любого идентификатора, разрешенного SQLite, поэтому решение методом проб и ошибок для меня слишком неопределенно.

SQLite использует " для кавычек идентификаторов , но я не уверен, что достаточно просто экранировать их. Документация PHP sqlite_escape_string предполагает, что некоторые двоичные данные также могут быть экранированы, но это может быть особенностью библиотеки PHP.

Ответы [ 9 ]

30 голосов
/ 15 июля 2011

Чтобы преобразовать любую строку в идентификатор SQLite:

  • Убедитесь, что строка может быть закодирована как UTF-8.
  • Убедитесь, что строка не содержит символов NUL.
  • Заменить все " на "".
  • Оберните все это в двойные кавычки.

Осуществление

import codecs

def quote_identifier(s, errors="strict"):
    encodable = s.encode("utf-8", errors).decode("utf-8")

    nul_index = encodable.find("\x00")

    if nul_index >= 0:
        error = UnicodeEncodeError("NUL-terminated utf-8", encodable,
                                   nul_index, nul_index + 1, "NUL not allowed")
        error_handler = codecs.lookup_error(errors)
        replacement, _ = error_handler(error)
        encodable = encodable.replace("\x00", replacement)

    return "\"" + encodable.replace("\"", "\"\"") + "\""

Если в строке указан один аргумент, он будет экранирован и правильно указан в кавычках или вызовет исключение Второй аргумент может использоваться для указания любого обработчика ошибок, зарегистрированного в модуле codecs . Встроенные:

  • 'strict': вызвать исключение в случае ошибки кодирования
  • 'replace': заменить искаженные данные подходящим маркером замены, таким как '?' или '\ufffd'
  • 'ignore': игнорировать искаженные данные и продолжить без дальнейшего уведомления
  • 'xmlcharrefreplace': заменить на соответствующую ссылку на символ XML (только для кодирования)
  • 'backslashreplace': заменить escape-последовательностями с обратной косой чертой (только для кодирования)

Это не проверяет зарезервированные идентификаторы, поэтому, если вы попытаетесь создать новую таблицу SQLITE_MASTER, это не остановит вас.

Пример использования

import sqlite3

def test_identifier(identifier):
    "Tests an identifier to ensure it's handled properly."

    with sqlite3.connect(":memory:") as c:
        c.execute("CREATE TABLE " + quote_identifier(identifier) + " (foo)")
        assert identifier == c.execute("SELECT name FROM SQLITE_MASTER").fetchone()[0]

test_identifier("'Héllo?'\\\n\r\t\"Hello!\" -☃") # works
test_identifier("北方话") # works
test_identifier(chr(0x20000)) # works

print(quote_identifier("Fo\x00o!", "replace")) # prints "Fo?o!"
print(quote_identifier("Fo\x00o!", "ignore")) # prints "Foo!"
print(quote_identifier("Fo\x00o!")) # raises UnicodeEncodeError
print(quote_identifier(chr(0xD800))) # raises UnicodeEncodeError

Наблюдения и справки

  • Идентификаторы SQLite TEXT, а не двоичные.
    • SQLITE_MASTER схема в FAQ
    • Python 2 SQLite API кричал на меня, когда я давал ему байты, которые он не мог декодировать как текст.
    • Python 3 SQLite API требует, чтобы запросы были str с, а не bytes.
  • Двойные кавычки в идентификаторах SQLite экранируются как две двойные кавычки.
  • Идентификаторы SQLite сохраняют регистр, но они не чувствительны к регистру букв ASCII. Можно включить Unicode-учитывающую регистр нечувствительность.
  • sqlite3 может обрабатывать любую другую строку Unicode, если она может быть правильно закодирована в UTF-8. Неверные строки могут вызвать сбои между Python 3.0 и Python 3.1.2 или около того. Python 2 принял эти недопустимые строки, но это считается ошибкой.
27 голосов
/ 29 июня 2011

Документация psycopg2 явно рекомендует использовать обычный формат python% или {} для замены в именах таблиц и столбцов (или других битах динамического синтаксиса), а затем использовать механизм параметров для подстановки значений в запрос.

Я не согласен со всеми, кто говорит: «Никогда не используйте динамические имена таблиц / столбцов, вы делаете что-то неправильно, если вам нужно».Я пишу программы для автоматизации вещей с базами данных каждый день, и я делаю это все время.У нас есть много баз данных с большим количеством таблиц, но все они построены на повторяющихся шаблонах, поэтому универсальный код для их обработки чрезвычайно полезен.Почерк запросов каждый раз будет намного более подвержен ошибкам и опасен.

Все сводится к тому, что означает «безопасный».Общепринятое мнение заключается в том, что использование обычных манипуляций с питоном для ввода значений в ваши запросы не является «безопасным».Это происходит потому, что если вы делаете это, то все может пойти не так, и такие данные очень часто поступают от пользователя и не находятся под вашим контролем.Вам нужен 100% надежный способ правильного экранирования этих значений, чтобы пользователь не мог вставить SQL в значение данных и заставить базу данных выполнить его.Так что авторы библиотеки делают эту работу;Вы никогда не должны.

Если, однако, вы пишете универсальный вспомогательный код для работы с вещами в базах данных, то эти соображения не так уж применимы.Вы неявно предоставляете любому, кто может вызвать такой код, доступ ко всему в базе данных; в этом смысл вспомогательного кода .Таким образом, теперь проблема безопасности заключается в том, чтобы сгенерированные пользователем данные никогда не могли использоваться в таком коде.Это общая проблема безопасности при кодировании, и это та же проблема, что и вслепую exec при вводе строки пользователем.Это отдельная проблема от вставки значений в ваши запросы, потому что там вы хотите , чтобы иметь возможность безопасно обрабатывать вводимые пользователем данные.

Поэтому я рекомендую: делайте все, что хотите, для динамической сборки ваших запросов.Используйте обычные шаблоны строк Python для подстановки в именах таблиц и столбцов, склейте, где предложения и объединения, все хорошие (и ужасные для отладки) вещи.Но убедитесь, что вы знаете, что любые значения, к которым обращается такой код, должны исходить от вас , а не от ваших пользователей [1].Затем вы используете функциональность подстановки параметров SQLite, чтобы безопасно вставлять введенные пользователем значения в свои запросы как значения.

[1] Если (как в случае с большим количеством кода, который я пишу)ваши пользователи являются людьми, которые в любом случае имеют полный доступ к базам данных, и этот код призван упростить их работу, тогда это соображение на самом деле не применимо;вы, вероятно, собираете запросы по указанным пользователем таблицам.Но вы все равно должны использовать подстановку параметров SQLite, чтобы уберечь себя от неизбежного подлинного значения, которое в конечном итоге содержит кавычки или знаки процента.

16 голосов
/ 29 июня 2011

Если вы абсолютно уверены, что вам нужно динамически указывать имена столбцов, вам следует использовать библиотеку, которая может сделать это безопасно (и жалуется на неправильные вещи). SQLAlchemy очень хорош в этом.

>>> import sqlalchemy
>>> from sqlalchemy import *
>>> metadata = MetaData()
>>> dynamic_column = "cow"
>>> foo_table = Table('foo', metadata,
...     Column(dynamic_column, Integer))
>>> 

foo_table сейчас представляет таблицу с динамической схемой, но вы можете использовать ее только в контексте фактического соединения с базой данных (чтобы sqlalchemy знал диалект и что делать с сгенерированным SQL).

>>> metadata.bind = create_engine('sqlite:///:memory:', echo=True)

Затем вы можете выдать CREATE TABLE .... с echo=True sqlalchemy будет log сгенерированным sql, но в целом sqlalchemy старается изо всех сил удержать сгенерированный sql в ваших руках (чтобы вы не решили использовать его для злых целей). *

>>> foo_table.create()
2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c 
CREATE TABLE foo (
    cow INTEGER
)
2011-06-28 21:54:54,040 INFO sqlalchemy.engine.base.Engine.0x...2f4c ()
2011-06-28 21:54:54,041 INFO sqlalchemy.engine.base.Engine.0x...2f4c COMMIT
>>> 

и да, sqlalchemy позаботится о любых именах столбцов, которые требуют специальной обработки, например, когда имя столбца является зарезервированным словом sql

>>> dynamic_column = "order"
>>> metadata = MetaData()
>>> foo_table = Table('foo', metadata,
...     Column(dynamic_column, Integer))
>>> metadata.bind = create_engine('sqlite:///:memory:', echo=True)
>>> foo_table.create()
2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c 
CREATE TABLE foo (
    "order" INTEGER
)
2011-06-28 22:00:56,267 INFO sqlalchemy.engine.base.Engine.0x...aa8c ()
2011-06-28 22:00:56,268 INFO sqlalchemy.engine.base.Engine.0x...aa8c COMMIT
>>> 

и может спасти вас от возможного вреда:

>>> dynamic_column = "); drop table users; -- the evil bobby tables!"
>>> metadata = MetaData()
>>> foo_table = Table('foo', metadata,
...     Column(dynamic_column, Integer))
>>> metadata.bind = create_engine('sqlite:///:memory:', echo=True)
>>> foo_table.create()
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec 
CREATE TABLE foo (
    "); drop table users; -- the evil bobby tables!" INTEGER
)
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec ()
2011-06-28 22:04:22,051 INFO sqlalchemy.engine.base.Engine.0x...05ec COMMIT
>>> 

(по-видимому, некоторые странные вещи являются совершенно легальными идентификаторами в sqlite)

7 голосов
/ 14 июля 2011

Первое, что нужно понять 1002 *, это то, что имена таблиц / столбцов не могут быть экранированы в том же смысле, что и строки, хранящиеся в качестве значений базы данных.

Причина в том, что вы должны:

  • принять / отклонить потенциальное имя таблицы / столбца, т.е. не гарантируется, что строка является приемлемым именем столбца / таблицы,в отличие от строки, которая будет храниться в какой-либо базе данных;или
  • очистите строку, что будет иметь тот же эффект, что и при создании дайджеста: используется функция сюръективная , а не биективная (еще раз, обратное вернодля строки, которая должна быть сохранена в некоторой базе данных);поэтому вы не только не можете быть уверены, что вернетесь от очищенного имени к исходному имени, но вы рискуете непреднамеренно попытаться создать два столбца или таблицы с одинаковым именем.

ИмеяСледует понимать, что секунда , которую нужно понять, заключается в том, что то, как вы в конечном итоге «избежите» имен таблиц / столбцов, зависит от вашего конкретного контекста, и поэтому существует более одного способа сделать это, но независимо от способавам нужно будет покопаться, чтобы выяснить, что является или не является приемлемым именем столбца / таблицы в sqlite.

Для начала, вот одно условие:

Имена таблиц, начинающиеся с "sqlite_", зарезервированы для внутреннего использования.Ошибка при попытке создать таблицу с именем, начинающимся с «sqlite _».

Еще лучше, использование определенных имен столбцов может иметь непредвиденные побочные эффекты:

Каждая строка каждой таблицы SQLite имеет 64-битный целочисленный ключ со знаком, который однозначно идентифицирует строку в своей таблице.Это целое число обычно называют «rowid».Доступ к значению rowid можно получить с помощью одного из специальных независимых от регистра имен «rowid», «oid» или « rowid » вместо имени столбца.Если таблица содержит определенный пользователем столбец с именами «rowid», «oid» или « rowid », то это имя всегда ссылается на явно объявленный столбец и не может использоваться для получения целочисленного значения rowid.

Оба цитируемых текста взяты из http://www.sqlite.org/lang_createtable.html

6 голосов
/ 14 июля 2011

Из sqlite faq, вопрос 24 (формулировка вопроса, конечно, не дает подсказки, что ответ может быть полезен для вашего вопроса):

SQLиспользует двойные кавычки вокруг идентификаторов (имен столбцов или таблиц), которые содержат специальные символы или являются ключевыми словами.Таким образом, двойные кавычки - это способ избежать имен идентификаторов.

Если само имя содержит двойные кавычки, избегайте этой двойной кавычки с другой.

5 голосов
/ 29 июня 2011

Заполнители только для значений. Имена столбцов и таблиц являются структурными и похожи на имена переменных; Вы не можете использовать заполнители для их заполнения.

У вас есть три варианта:

  1. Правильно экранируйте / цитируйте имя столбца везде, где вы его используете. Это хрупкое и опасное.
  2. Используйте ORM, например SQLAlchemy , который позаботится о экранировании / цитировании для вас.
  3. В идеале, просто не иметь динамических имен столбцов. Таблицы и столбцы для структура ; что-нибудь динамическое - это data и должно быть в таблице, а не в ее части.
1 голос
/ 04 июля 2019

Используйте функцию, определенную специально для этого:

цитата (X)

Функция quote (X) возвращает текст литерала SQL, который является значением его аргумента и подходит для включения в оператор SQL. Строки окружены одинарными кавычками с экранированием по внутренним кавычкам при необходимости. BLOB кодируются как шестнадцатеричные литералы. Строки со встроенными символами NUL не могут быть представлены в SQL как строковые литералы, и поэтому возвращаемый строковый литерал усекается до первого NUL.

Источник: https://www.sqlite.org/draft/lang_corefunc.html#quote

Пример использования:

db = sqlite3.connect('foo.sqlite3')
cur = db.cursor()
escaped_symbol = cur.execute('SELECT quote(?);', [unescaped_symbol]).fetchone()[0]
1 голос
/ 21 марта 2017

Начиная с psycopg2 версии 2.7 (выпущено в феврале 2017 г.), имена столбцов и имена таблиц (идентификаторы) могут быть сгенерированы на лету безопасным способом с использованием psycopg2.sql.Вот ссылка на документацию с примерами: http://initd.org/psycopg/docs/sql.html.

Таким образом, способ написать запрос в вашем вопросе будет:

import sqlite3
from psycopg2 import sql
with sqlite3.connect(":memory:") as connection:
    query = sql.SQL("CREATE TABLE {}").format("bar")
    connection.execute(query)
0 голосов
/ 29 июня 2011

Если вы обнаружите, что вам нужно переменное имя сущности (relvar или field), то вы, вероятно, что-то делаете неправильно . альтернативный шаблон будет использовать карту свойств, что-то вроде:

CREATE TABLE foo_properties(
    id INTEGER NOT NULL,
    name VARCHAR NOT NULL,
    value VARCHAR,
    PRIMARY KEY(id, name)
);

Затем вы просто динамически указываете имя при вставке вместо столбца.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...