SQLAlchemy default DateTime
Asked Answered
C

13

317

This is my declarative model:

import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Test(Base):
    __tablename__ = 'test'

    id = Column(Integer, primary_key=True)
    created_date = DateTime(default=datetime.datetime.utcnow)

However, when I try to import this module, I get this error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "orm/models2.py", line 37, in <module>
    class Test(Base):
  File "orm/models2.py", line 41, in Test
    created_date = sqlalchemy.DateTime(default=datetime.datetime.utcnow)
TypeError: __init__() got an unexpected keyword argument 'default'

If I use an Integer type, I can set a default value. What's going on?

Celik answered 13/11, 2012 at 22:55 Comment(2)
This shouldn't be used. utcnow is a naive timestamp in UTC timezone, however, chances are that naive timestamps are interpreted in local timezone instead.Horrific
I know this question was asked a long time ago but I think your answer should be changed to the one that @Jeff Widman provided as the other will use the "compile" time datetime when the table class is defined versus when the record is created. If you are AFK then at least this comment will provide a warning for others to carefully check the comments for each question.Marlyn
L
281

DateTime doesn't have a default key as an input. The default key should be an input to the Column function. Try this:

import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Test(Base):
    __tablename__ = 'test'

    id = Column(Integer, primary_key=True)
    created_date = Column(DateTime, default=datetime.datetime.utcnow)
Lemuroid answered 13/11, 2012 at 23:1 Comment(9)
This isn't right. The timestamp at model load will be used for all new records rather than the time the record is added.Tardy
@Tardy This code is correct, you can test it here: pastebin.com/VLyWktUn . datetime.datetime.utcnow seems called as callback.Territorial
I used this answer but the timestamp at model load are being used for all new records.Joycejoycelin
@scottdelta: Make sure you aren't using default=datetime.datetime.utcnow(); you want to pass the utcnow function, not the result of evaluating it at module load.Brahear
Still did not work for me. The approach of jeff-widman worked for me: from sqlalchemy.sql import func; created_date = Column(DateTime(timezone=True), server_default=func.now()Engedi
I now understand the difference between the two methods discussed in this thread: server_default will result in a default value stored at the SQL table definition level (you can see it if you do \d <your_table> in psql for instance), and which would be taken into account while using a raw insert query, outside the ORM. On the other hand, default will only be working at the ORM level: if you create a model object and save it, it will contain the default value (because the ORM has explicitly set it), while a raw insert query would result in a record without the default value.Enterovirus
I forgot to mention it in my previous comment, and I feel it's important somehow (I cannot edit my comment anymore unfortunately): what I describe is true in the context of a Postgres database; I'm not certain it applies to other databases though.Enterovirus
this code also fails to append the timezone. Since Python datetime does not include the tz, these timestamps can't be read from a javascript frontend, for instance.Odoriferous
@ChristianDavis if you need the timezone use a lambda instead of directly referencing the function. eg: Column(Datetime, lambda: default=datetime.datetime.now(datetime.timezone.utc))Darien
E
704

Calculate timestamps within your DB, not your client

For sanity, you probably want to have all datetimes calculated by your DB server, rather than the application server. Calculating the timestamp in the application can lead to problems because network latency is variable, clients experience slightly different clock drift, and different programming languages occasionally calculate time slightly differently.

SQLAlchemy allows you to do this by passing func.now() or func.current_timestamp() (they are aliases of each other) which tells the DB to calculate the timestamp itself.

Use SQLALchemy's server_default

Additionally, for a default where you're already telling the DB to calculate the value, it's generally better to use server_default instead of default. This tells SQLAlchemy to pass the default value as part of the CREATE TABLE statement.

For example, if you write an ad hoc script against this table, using server_default means you won't need to worry about manually adding a timestamp call to your script--the database will set it automatically.

Understanding SQLAlchemy's onupdate/server_onupdate

SQLAlchemy also supports onupdate so that anytime the row is updated it inserts a new timestamp. Again, best to tell the DB to calculate the timestamp itself:

from sqlalchemy.sql import func

time_created = Column(DateTime(timezone=True), server_default=func.now())
time_updated = Column(DateTime(timezone=True), onupdate=func.now())

There is a server_onupdate parameter, but unlike server_default, it doesn't actually set anything serverside. It just tells SQLalchemy that your database will change the column when an update happens (perhaps you created a trigger on the column ), so SQLAlchemy will ask for the return value so it can update the corresponding object.

One other potential gotcha:

You might be surprised to notice that if you make a bunch of changes within a single transaction, they all have the same timestamp. That's because the SQL standard specifies that CURRENT_TIMESTAMP returns values based on the start of the transaction.

PostgreSQL provides the non-SQL-standard statement_timestamp() and clock_timestamp() which do change within a transaction. Docs here: https://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT

UTC timestamp

If you want to use UTC timestamps, a stub of implementation for func.utcnow() is provided in SQLAlchemy documentation. You need to provide appropriate driver-specific functions on your own though.

Euhemerus answered 4/11, 2015 at 21:15 Comment(21)
I totally agree with you. However, my setup does not work. ``` class TimestampMixin(object): created_at = Column('created_at', DateTime(timezone=True), default=func.now()) updated_at = Column('updated_at', DateTime(timezone=True), default=func.now(), onupdate=func.now()) ``` I issued a create and an update statement 1 second apart. The two values for created_at and updated_at are always the same.Forgather
IIRC, in PostgreSQL, if you issue both of them within the same DB transaction the timestamps will both be from the transaction start time. Not sure about other DB's. You may also not have flushed and/or committed the SQLAlchemy session in between the create/update statement. If not either of those, then it will be far easier to debug if you open a new question. Feel free to include a link here and I'll take a look.Euhemerus
I did see there's now a way to tell Postgres to use the current time, not just the transaction start time... it's in the postgres docsEuhemerus
is there a func.utcnow() or something like that?Pudding
@JeffWidman: Do we have a mysql implementation for utcnow? I in documentation only mssql and postregMerta
@Merta I don't know off top of my head. If you open a new question and link to it here then I can try to look it up.Euhemerus
@JeffWidman: You are welcome, #47555158Merta
Got sqlalchemy.exc.OperationalError: (_mysql_exceptions.OperationalError) (1067, "Invalid default value for 'created'")Confront
how to set default datetime to a fixed datetime like 1970-1-1?Publicity
server_default=func.now() worked for me, but onupdate=func.now() didn't. Tried onupdate=datetime.datetime.utcnow (without brackets), that also didnt helpFavian
tried server_onupdate=func.now() too. That didnt work either.Favian
I finally got it working on Postgres db by doing this: created_at = db.Column(db.DateTime, server_default=UtcNow()) and updated_at = db.Column(db.DateTime, server_default=UtcNow(), onupdate=UtcNow()) where UtcNow is a class as class UtcNow(expression.FunctionElement): type = DateTime() and we have @compiles(UtcNow, 'postgresql') def pg_utc_now(element, compiler, **kw): return "TIMEZONE('utc', CURRENT_TIMESTAMP)"Favian
@VikasPrasad - I don't think the onupdate will generate the column definition you expect; meaning, it won't update the timestamp on record 'update'. You need a DB function + update trigger to do this.Abbyabbye
great answer - thing to note, at least for me - the insert ts only started working after i re-created the table. instead of re-creating the table - you could also modify your table in mysql, and add an expression (last column) saying CURRENT_TIMESTAMPHuei
@JeffWidman (or anyone else who knows), assuming you set up the before update trigger properly, does it matter what you put in server_onupdate then if it's just a signal to SQLAlchemy to pull the new value from the DB?Welford
Works great but I just want to point out that for me, the update timestamp does not get filled in upon creation. So, if you want all recently edited records, you would have to look at both time_created and time_updated. This was using sqlite and flask-sqlalchemy.Sect
server_default=func.now() did not work with sqlalchemy + sqlite3 . datetime solution workedPilferage
@JeffWidman I found an open source implementation for UTC that seems to work for many DB drivers. Its name is SQLAlchemy-Utc. See my answer here https://mcmap.net/q/98961/-sqlalchemy-default-datetimeOlmos
Regarding "If you want to use UTC timestamps," - aren't all timestamps UTC? Or does @JeffWidman mean if you want to retrieve (as well as store) UTC timestamps?Telegu
can we use this approach without setting timezone=true?Defalcate
funt is not defined, which module this comes from?Defalcate
L
281

DateTime doesn't have a default key as an input. The default key should be an input to the Column function. Try this:

import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Test(Base):
    __tablename__ = 'test'

    id = Column(Integer, primary_key=True)
    created_date = Column(DateTime, default=datetime.datetime.utcnow)
Lemuroid answered 13/11, 2012 at 23:1 Comment(9)
This isn't right. The timestamp at model load will be used for all new records rather than the time the record is added.Tardy
@Tardy This code is correct, you can test it here: pastebin.com/VLyWktUn . datetime.datetime.utcnow seems called as callback.Territorial
I used this answer but the timestamp at model load are being used for all new records.Joycejoycelin
@scottdelta: Make sure you aren't using default=datetime.datetime.utcnow(); you want to pass the utcnow function, not the result of evaluating it at module load.Brahear
Still did not work for me. The approach of jeff-widman worked for me: from sqlalchemy.sql import func; created_date = Column(DateTime(timezone=True), server_default=func.now()Engedi
I now understand the difference between the two methods discussed in this thread: server_default will result in a default value stored at the SQL table definition level (you can see it if you do \d <your_table> in psql for instance), and which would be taken into account while using a raw insert query, outside the ORM. On the other hand, default will only be working at the ORM level: if you create a model object and save it, it will contain the default value (because the ORM has explicitly set it), while a raw insert query would result in a record without the default value.Enterovirus
I forgot to mention it in my previous comment, and I feel it's important somehow (I cannot edit my comment anymore unfortunately): what I describe is true in the context of a Postgres database; I'm not certain it applies to other databases though.Enterovirus
this code also fails to append the timezone. Since Python datetime does not include the tz, these timestamps can't be read from a javascript frontend, for instance.Odoriferous
@ChristianDavis if you need the timezone use a lambda instead of directly referencing the function. eg: Column(Datetime, lambda: default=datetime.datetime.now(datetime.timezone.utc))Darien
A
87

You can also use sqlalchemy builtin function for default DateTime

from sqlalchemy.sql import func

DT = Column(DateTime(timezone=True), default=func.now())
Adit answered 6/5, 2015 at 17:17 Comment(5)
You can also use server_default instead of default so value will by handled by database itself.Allowance
Isn't func.now() executed when the descriptor is defined, so all models/children (if this is a base class) will have the same DT value. By passing in a function reference like 'datetime.datetime.utcnow' it is executed separately for each. This is crucial for 'created' or 'updated' properties.Gainer
@Gainer No, this is correct. sqlalchemy.sql.func is a special case that returns a sqlalchemy.sql.functions.now instance, not the current time. docs.sqlalchemy.org/en/latest/core/…Evannia
What does timezone=True do?Diseuse
@self, From the documentation: "If True, and supported by the backend, will produce ‘TIMESTAMP WITH TIMEZONE’. For backends that don’t support timezone aware timestamps, has no effect. .Diseuse
M
15

Using the default parameter with datetime.now:

from sqlalchemy import Column, Integer, DateTime
from datetime import datetime
class Test(Base):
     __tablename__ = 'test'
     id = Column(Integer, primary_key=True)
     created_at = Column(DateTime, default=datetime.now)
     updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)                                                            
Molly answered 7/5, 2021 at 6:9 Comment(2)
Please format your code properly and may be exaplain a little on how it solves the issue.Evenhanded
default=datetime.now in the column , will save time when inserting the records.Molly
R
14

You likely want to use onupdate=datetime.now so that UPDATEs also change the last_updated field.

SQLAlchemy has two defaults for python executed functions.

  • default sets the value on INSERT, only once
  • onupdate sets the value to the callable result on UPDATE as well.
Retiarius answered 17/7, 2015 at 21:24 Comment(5)
I dont know why, but for some reason onupdate doesn't do anything for me.Favian
I finally got it working on Postgres db by doing this: created_at = db.Column(db.DateTime, server_default=UtcNow()) and updated_at = db.Column(db.DateTime, server_default=UtcNow(), onupdate=UtcNow()) where UtcNow is a class as class UtcNow(expression.FunctionElement): type = DateTime() and we have @compiles(UtcNow, 'postgresql') def pg_utc_now(element, compiler, **kw): return "TIMEZONE('utc', CURRENT_TIMESTAMP)"Favian
I believe sqlalchmey.sql.func.now() is preferred, though won't be too different. It's slightly better to use DB time instead of server time if the system is distributed & many different users may make changes around the same timeSis
is there a way to disable the onupdate call for python-executed functions? docs.sqlalchemy.org/en/14/core/defaults.htmlVersicle
I did this same thing and I got an error due to not passing any args into UtcNow in the DB model.Pecker
T
8

The default keyword parameter should be given to the Column object.

Example:

Column(u'timestamp', TIMESTAMP(timezone=True), primary_key=False, nullable=False, default=time_now),

The default value can be a callable, which here I defined like the following.

from pytz import timezone
from datetime import datetime

UTC = timezone('UTC')

def time_now():
    return datetime.now(UTC)
Tuinal answered 13/11, 2012 at 23:1 Comment(4)
Where does time_now come from?Transmogrify
Thank you! I assumed that there a built-in function with that name that I somehow overlooked.Transmogrify
or datetime.datetime.utcnowMcgannon
from datetime import datetime as dt .... then just dt.utcnow()Odoacer
G
2

For mariadb thats worked for me:

from sqlalchemy import Column, Integer, String, DateTime, TIMESTAMP, text
from sqlalchemy.sql import func
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Test(Base):
  __tablename__ = "test"

  id              = Column(Integer, primary_key=True, autoincrement=True)
  name            = Column(String(255), nullable=False)
  email           = Column(String(255), nullable=False)
  created_at      = Column(TIMESTAMP, nullable=False, server_default=func.now())
  updated_at      = Column(DateTime, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))

In the sqlalchemy documentation for mariadb, it is recommended to import the textfrom sqlalchemy itself and set the server_default with the text, inserting the custom command.

updated_at=Column(DateTime, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))

To understand func.now you can read the sql alchemy documentation.

Hope I helped in some way.

Geest answered 26/9, 2021 at 20:0 Comment(0)
E
2

TL;DR - version controlled database triggers to automatically update updated_at field, and make the ORM aware and issue a RETURNING statement.

WHY? - because we want to avoid relying purely on ORM's onupdate to make suer update_at field is up-to-date.

NOTE: this code was designed for PostgreSQL, but would be similar for other databases.

To extend on this answer, for a default value you should use server_default as suggested. However, for having a value that will be updated upon an UPDATE statement, you have 2 options. First is described in the linked answer. The second option is using database triggers. I found alembic-utils useful to make sure triggered is migrated and updated when the version-controlled is modified. Combining it with a Base model and table listing allowed adding those created/updated fields to all ORM models.

from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import func, FetchedValue


class Base(DeclarativeBase):
    __abstract__ = True
    metadata = MetaData(naming_convention=convention)

    id: Mapped[int] = mapped_column(Identity(), primary_key=True)

    created_at: Mapped[datetime] = mapped_column(server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(
        server_default=FetchedValue(), server_onupdate=FetchedValue()
    )

    # IMPORTANT! see details below
    __mapper_args__ = {"eager_defaults": True}


class Customer(Base):
    __tablename__ = "customers"
    name: Mapped[str] = mapped_column(nullable=False)

triggers.py:

from alembic_utils.pg_function import PGFunction
from alembic_utils.pg_trigger import PGTrigger
from alembic_utils.replaceable_entity import register_entities

# import your `Base` module - ONLY AFTER all other modules were imported as well
# that's anyway required for handling the metadata in `alembic`. 
from ... import Base

update_update_time_func = PGFunction(
    schema="public",
    signature="update_updated_at()",
    definition="""
        RETURNS TRIGGER AS $$
        BEGIN
            NEW.updated_at = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC');
            RETURN NEW;
        END;
        $$ LANGUAGE plpgsql
    """,
)

register_entities([update_update_time_func])

update_time_triggers = [
    PGTrigger(
        schema="public",
        signature=f"update_{tb_name}_updated_at",
        on_entity=tb_name,
        definition=f"""
            BEFORE INSERT OR UPDATE ON {tb_name}
            FOR EACH ROW EXECUTE FUNCTION update_updated_at()
        """,
    )
    for tb_name, tb in Base.metadata.tables.items()
    if any(c.name == "updated_at" for c in tb.columns)
]

register_entities(update_time_triggers)

In migrations/env.py import your triggers module. It will register the trigger entities and the stored procedure.

running alembic revision --autogenerate -m "add triggers" should identify the function and triggers, and alembic upgrade head should add the function and triggers.

Eager defaults and performance: implementing this solution, it was important for me both:

  1. After an INSERT or UPDATE operation (done via ORM), the instance at hand will be updated by the database-generated values (created and updated timestamps, in this case).
  2. The operation will not trigger a SELECT, but instead use RETURNING where appropriate.

To demonstrate the above consider the following code:

with Session() as session:
    c = Customer(name='foo')
    session.add(c)
    print(f'Instance before INSERT: {c}')
    print('INSERT logs:')
    session.commit()
    print(f'Instance after INSERT: {c}')
    c.name = 'bar'
    print('UPDATE logs:')
    session.commit()
    print(f'Instance after UPDATE: {c}')   

Which produces these logs (slightly edited for readability):

Instance before INSERT: 
<Customer(name='foo')>
INSERT logs:
    BEGIN (implicit)
    INSERT INTO customers (name) VALUES (%(name)s) RETURNING customers.id, customers.created_at, customers.updated_at
    [cached since 1.849e+04s ago] {'name': 'foo'}
    COMMIT
Instance after INSERT: 
<Customer(name='foo', id=8, created_at=datetime.datetime(2023, 12, 5, 14, 23, 3, 964627), updated_at=datetime.datetime(2023, 12, 5, 14, 23, 3, 964627))>
UPDATE logs:
    BEGIN (implicit)
    UPDATE customers SET name=%(name)s WHERE customers.id = %(customers_id)s RETURNING customers.updated_at
    [cached since 220s ago] {'name': 'bar', 'customers_id': 8}
    COMMIT
Instance after UPDATE: 
<Customer(name='bar', id=8, created_at=datetime.datetime(2023, 12, 5, 14, 23, 3, 964627), updated_at=datetime.datetime(2023, 12, 5, 14, 23, 3, 970578))>
Erythrocyte answered 5/12, 2023 at 14:25 Comment(0)
N
1

As per PostgreSQL documentation:

now, CURRENT_TIMESTAMP, LOCALTIMESTAMP return the time of transaction

This is considered a feature: the intent is to allow a single transaction to have a consistent notion of the "current" time, so that multiple modifications within the same transaction bear the same time stamp.

You might want to use statement_timestamp or clock_timestamp if you don't want transaction timestamp.

statement_timestamp()

returns the start time of the current statement (more specifically, the time of receipt of the latest command message from the client). statement_timestamp

clock_timestamp()

returns the actual current time, and therefore its value changes even within a single SQL command.

Nacelle answered 19/2, 2018 at 13:12 Comment(0)
O
0

Jeff Widman said on his answer that you need to create your own implementation of UTC timestamps for func.utcnow()

As I didnt want to implement it myself, I have searched for and found a python package which already does the job and is maintained by many people.

The package name is spoqa/sqlalchemy-ut.

A summary of what the package does is: Long story short, UtcDateTime does:

take only aware datetime.datetime, return only aware datetime.datetime, never take or return naive datetime.datetime, ensure timestamps in database always to be encoded in UTC, and work as you’d expect.

Olmos answered 30/7, 2021 at 9:27 Comment(0)
O
0

Note that for server_default=func.now() and func.now() to work :

Local_modified = Column(DateTime, server_default=func.now(), onupdate=func.now())

you need to set DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP in your table DDL.

For example

create table test
(
    id int auto_increment
        primary key,
    source varchar(50) null,
    Local_modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
collate=utf8mb4_bin;

Otherwise, server_default=func.now(), onupdate=func.now() makes no effects.

Ossicle answered 29/9, 2021 at 15:49 Comment(1)
'ON UPDATE' is a mysql specific syntax; dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.htmlSheeb
M
-1

You can use TIMESTAMP with sqlalchemy.

from sqlalchemy import TIMESTAMP, Table, MetaData, Column, ...

... ellipsis ...  
def function_name(self) -> Table:  
    return Table(  
        "table_name",  
        self._metadata,  
        ...,
        Column("date_time", TIMESTAMP),  
    )  
... ellipsis ...  
Maggs answered 23/12, 2022 at 9:43 Comment(0)
C
-1

It's better to use like this way,,

from sqlalchemy import DateTime
from datetime import datetime  # Import datetime

created_at = Column(DateTime, default=datetime.datetime.now())
updated_at = Column(DateTime, onupdate=datetime.datetime.now())

Sorry for the Previous version. Now it's perfectly works for me...

created_at = Column(DateTime, default=datetime.datetime.now())
updated_at = Column(DateTime, default=datetime.datetime.now(), onupdate=datetime.datetime.now())

Here is my full user model

import uuid
from app.configs.database import Base
from sqlalchemy import Column, String, Integer, DateTime, Enum
from app.utils.utils import ERoles
import datetime


class User(Base):
    __tablename__ = "Users"

    id = Column(Integer, primary_key=True, index=True)
    uuid = Column(String(36), unique=True,
                  default=str(uuid.uuid4()))
    name = Column(String(256), nullable=True)
    username = Column(String(256), unique=True)
    email = Column(String(256), unique=True)
    password = Column(String(256))
    role = Column(Enum(ERoles), default=ERoles.USER)

    created_at = Column(DateTime, default=datetime.datetime.now())
    updated_at = Column(DateTime, default=datetime.datetime.now(),
                        onupdate=datetime.datetime.now())

Curd answered 14/10, 2023 at 4:2 Comment(1)
This approach will cause the timestamp values to be computed once, when the module is imported or executed. This is almost never the desired behaviour.Filemon

© 2022 - 2024 — McMap. All rights reserved.