在 psycopg2 中将表名作为参数传递

Posted

技术标签:

【中文标题】在 psycopg2 中将表名作为参数传递【英文标题】:Passing table name as a parameter in psycopg2 【发布时间】:2012-11-27 10:05:43 【问题描述】:

我有以下代码,使用 pscyopg2:

sql = 'select %s from %s where utctime > %s and utctime < %s order by utctime asc;'
data = (dataItems, voyage, dateRangeLower, dateRangeUpper)
rows = cur.mogrify(sql, data)

这个输出:

select 'waterTemp, airTemp, utctime' from 'ss2012_t02' where utctime > '2012-05-03T17:01:35+00:00'::timestamptz and utctime < '2012-05-01T17:01:35+00:00'::timestamptz order by utctime asc;

当我执行此操作时,它会倒下 - 这是可以理解的,因为表名周围的引号是非法的。

有没有办法合法地将表名作为参数传递,或者我需要做一个(明确警告)字符串连接,即:

voyage = 'ss2012_t02'
sql = 'select %s from ' + voyage + ' where utctime > %s and utctime < %s order by utctime asc;'

为任何见解喝彩。

【问题讨论】:

【参考方案1】:

根据官方文档:

如果您需要动态生成 SQL 查询(例如 动态选择表名)您可以使用这些设施 由 psycopg2.sql 模块提供。

sql 模块是 psycopg2 2.7 版中的新模块。它的语法如下:

from psycopg2 import sql

cur.execute(
    sql.SQL("insert into  values (%s, %s)")
        .format(sql.Identifier('my_table')),
    [10, 20])

更多信息:http://initd.org/psycopg/docs/sql.html#module-psycopg2.sql

[2017-03-24 更新:AsIs 不应用于表示表或字段名称,应使用新的 sql 模块:https://***.com/a/42980069/5285608]

另外,根据 psycopg2 文档:

警告:从不、neverNEVER 使用 Python 字符串连接 (+) 或字符串参数插值 (%)将变量传递给 SQL 查询字符串。甚至在枪口下也没有。

【讨论】:

这是最新的答案 确保在 sql.SQL 对象上而不是在内部字符串上调用 .format(...)。我刚刚失去了一个小时,因为我有错误的括号。也就是说,SQL('select * from '.format(...)) 不起作用,SQL('select * from ').format(...) 会。【参考方案2】:

根据this answer,您可以这样做:

import psycopg2
from psycopg2.extensions import AsIs

#Create your connection and cursor...

cursor.execute("SELECT * FROM %(table)s", "table": AsIs("my_awesome_table"))

【讨论】:

它应该是cursor.execute("SELECT * FROM %(table)s", "table": AsIs("my_awesome_table")),但不能编辑,因为它只有 2 个字符 :) 不幸的是,AsIs 违背了参数化的目的。 不要使用用户数据。 AsIs 不应用于此目的:***.com/a/42980069/5285608【参考方案3】:

表名不能作为参数传递,但其他的都可以。因此,表名应该在您的应用程序中进行硬编码(不要接受输入或使用程序之外的任何内容作为名称)。您拥有的代码应该适用于此。

如果您有正当理由使用外部表名,请确保您不允许用户直接输入它。也许可以传递一个索引来选择一个表,或者可以以其他方式查找表名。但是,您对此保持警惕是正确的。这是可行的,因为周围的表名相对较少。找到一种方法来验证表名,你应该没问题。

可以做这样的事情,看看表名是否存在。这是一个参数化版本。只需确保执行此操作并在运行 SQL 代码之前验证输出。部分想法来自this answer。

SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' and table_name=%s LIMIT 1

【讨论】:

是的,表名确实需要从外部传入,实际上没有任何办法。不过,我确实有一个“有效”(即安全)表的列表,所以我可以对其进行检查以确保传递的参数是可接受的,以防止注入.. 其实想想,这应该是一个相当简单的SQL查询。我已经发布了这样一个查询,您可以将其移植到您的适配器以满足您的目的。 “表名不能作为参数传递”——这不是真的。您可以使用 .format 不带字符串发送。 ***.com/a/42947632/1888503 下面这个答案是正确的。 是的,当然@sage88,我同意。然而,它仍然是可能的。更好的措辞是:“虽然可以将表名作为参数传递,但由于可能存在 SQL 注入,因此不建议这样做”。【参考方案4】:

这是我过去使用的解决方法

query = "INSERT INTO %s (col_1, col_2) VALUES (%%s, %%s)" % table_name
cur.execute(query, (col_1_var, col_2_var))

希望对你有帮助:)

【讨论】:

【参考方案5】:

我创建了一个小实用程序,用于预处理带有变量表 (...) 名称的 SQL 语句:

from string import letters
NAMECHARS = frozenset(set(letters).union('.'))

def replace_names(sql, **kwargs):
    """
    Preprocess an SQL statement: securely replace table ... names
    before handing the result over to the database adapter,
    which will take care of the values.

    There will be no quoting of names, because this would make them
    case sensitive; instead it is ensured that no dangerous chars
    are contained.

    >>> replace_names('SELECT * FROM %(table)s WHERE val=%(val)s;',
    ...               table='fozzie')
    'SELECT * FROM fozzie WHERE val=%(val)s;'
    """
    for v in kwargs.values():
        check_name(v)
    dic = SmartDict(kwargs)
    return sql % dic

def check_name(tablename):
    """
    Check the given name for being syntactically valid,
    and usable without quoting
    """
    if not isinstance(tablename, basestring):
        raise TypeError('%r is not a string' % (tablename,))
    invalid = set(tablename).difference(NAMECHARS)
    if invalid:
        raise ValueError('Invalid chars: %s' % (tuple(invalid),))
    for s in tablename.split('.'):
        if not s:
            raise ValueError('Empty segment in %r' % tablename)

class SmartDict(dict):
    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            check_name(key)
            return key.join(('%(', ')s'))

SmartDict 对象为每个未知的key 返回%(key)s,并保留它们以进行值处理。该函数可以检查是否缺少任何引号字符,因为现在应该注意所有引用...

【讨论】:

check_name 函数当然可以扩展,例如使其检查tablename 与白名单。【参考方案6】:

如果你想将表名作为参数传递,你可以使用这个包装器:

class Literal(str):
    def __conform__(self, quote):
        return self

    @classmethod
    def mro(cls):
        return (object, )

    def getquoted(self):
        return str(self)

用法:cursor.execute("CREATE TABLE %s ...", (Literal(name), ))

【讨论】:

我认为这与内置的 AsIs 包装器相同。检查这个答案(***.com/a/28593246/1591957)【参考方案7】:

这是对@Antoine Dusséaux 回答的一个小补充。如果你想在 SQL 查询中传递两个(不带引号的)参数,你可以这样做:-

query = sql.SQL("select field from table where pkey = %s").format(
    field=sql.Identifier('my_name'),
    table=sql.Identifier('some_table'),
    pkey=sql.Identifier('id'))

根据文档,

通常您应该将查询的模板表示为 SQL 具有 样式占位符的实例并使用 format() 合并 可变部分放入其中,所有这些都必须是 Composable 子类。 您仍然可以在查询中使用 %s 样式的占位符并传递值 执行():这样的值占位符将不被格式()触及

来源:https://www.psycopg.org/docs/sql.html#module-usage

另外,请在编写查询时牢记this。

【讨论】:

【参考方案8】:

您可以只使用模块格式作为表名,然后使用常规参数化执行:

xlist = (column, table)
sql = 'select 0 from 1 where utctime > %s and utctime < %s order by utctime asc;'.format(xlist)

请记住,如果这暴露给最终用户,除非您为它编写代码,否则您将不会受到 SQL 注入的保护。

【讨论】:

根据 psycopg2 文档:“警告:Never, never, NEVER 使用 Python 字符串连接 (+) 或字符串参数插值 (%)将变量传递给 SQL 查询字符串。即使是在枪口下。”为此目的创建了新的 SQL 模块:***.com/a/42947632/5285608【参考方案9】:

很惊讶没有人提到这样做:

sql = 'select  from  where utctime >  and utctime <  order by utctime asc;'.format(dataItems, voyage, dateRangeLower, dateRangeUpper)
rows = cur.mogrify(sql)

format 放入不带引号的字符串。

【讨论】:

虽然技术上与此处的许多其他答案一样正确,但您没有提到 SQL 注入,这是主要问题。

以上是关于在 psycopg2 中将表名作为参数传递的主要内容,如果未能解决你的问题,请参考以下文章

在 Ms-Access 中将表名作为查询参数传递

表名作为 PostgreSQL 函数参数

存储过程,将表名作为参数传递

如何在 Oracle SQL Developer 的存储过程中将表名列表作为参数传递?如何使用 PLSQL VARRAY 或嵌套表?

将表名作为输入参数动态传递并使用它[重复]

在 MySQL 中:如何将表名作为存储过程和/或函数参数传递?